Дальше вы не пройдёте, пока не получите бумаги.
Ранее мы изучали разработку приложений под iOS - в данном же случае мы научимся работать с macOS.
Тут появляется множество отличий в разработке:
В этом упражнении мы создадим простую игру, в которой Вы будете угадывать числа, а затем получать сообщения о том, верный был ответ или нет. По игровой логик цифры в числе не повторяются и число может начинаться с нуля.
Эта игра называется
Быки и Коровы
, в честь угадывания результатов
Начните создавать новый проект и выберите macOS
:
Там же выберите Cocoa App
.
Теперь введите название приложения, как в обычном случае и проверьте, что стоит галочка у Use Storyboards
:
Если перейти в раскадровку, то Вы увидите несколько необычных вещей:
Выделите представление контроллера и задайте ему размеры:
250
270
Перетащите Text Field
на полотно и сделайте с ним следующее:
36
228
100
22
Enter a guess
textField
Учтите, что macOS для своих приложений использует систему координат с центром в нижнем левом углу и нормальными осями, как показано на рисунке
Добавим кнопку на полотно:
Push Button
- нажимная кнопкаSubmit
139
222
83
button
buttonPressed
Теперь добавим табличное представление на полотно и зададим параметры размеров:
0
0
250
209
Создайте аутлет для табличного представления tableView
.
Таблицы в macOS сильно отличаются от iOS - они могут содержать по несколько колонок. Если Вы откроете табличное представление в Document Outline
, то удивитесь, как много оно всего содержит. К сожалению для обратной совместимости со старым кодом, на macOS таблицы так и не были упрощены.
Щелкните по зоне заголовков в представлении и введите два заголовка:
Guess
Result
Выберите Table View
в иерархии документа (не прокручиваемое представление, не Clip View
, а именно табличное представление), а затем задайте в инспекторе атрибутов:
Resizing
и Reordering
)Vertical Grid
- Solid
- вертикальная сетка сплошнаяAlternating Rows
, что сделает задний фон у ячеек чередующимся - это улучшает восприятие информацииТеперь добавим недостающие ограничения в атоматическом режиме:
Reset to Suggested Constraints
Это добавит автоматические ограничения.
Также отдельно задайте ограничение на ширину в 250
точек таблице (ее оборачивающему представлению) через AutoLayout, это позволит нашему приложению иметь фиксированную ширину и не сжиматься меньше.
И последним штрихом щелкните по окну в контроллере окон и в инспекторе атрибутов задайте заголовок окна либо скройте его (Hide Title
).
Теперь запустите приложение и Вы получите следующее:
Заметьте, что приложения при разработке в macOS запускаются не в симуляторе, а напрямую.
Установите источником и делегатом для табличного представления сам контроллер представления.
А теперь запустите приложение. Вы ожидали падения приложения, так как контроллер ещё не соответствует протоколам?
Не тут-то было: Вы просто получите в консоли сообщение:
2018-03-30 13:07:24.911910+0500 CowNBulls[2694:351966] *** Illegal NSTableView data source (<CowNBulls.ViewController: 0x6000000e2400>).
Must implement numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:
Если в iOS Вы получите ошибку, то macOS считает что несерьезной проблемой и просто уведомит Вас. Но не пользователя.
Теперь создадим расширение для контроллера представления, в котором подпишем его на протоколы источника данных и делегата;
extension ViewController: NSTableViewDelegate, NSTableViewDataSource {
}
Заметьте, что классы в macOS имеют префиксы
NS
, а неUI
.
В отличие от iOS эти протоколы не содержат обязательных методов.
Реализуем логику игры. Создайте два переменных свойства контроллера - строку answer
и массив строк guesses
:
var answer = ""
var guesses = [String]()
Прежде, чем двигаться дальше мы импортируем новый для нас Фреймворк:
import GameplayKit
Он содержит различные объекты для создания игровой логики. Мы же используем его для более качественной генерации случайных чисел, чем мы делали ранее.
Добавьте новый метод в контроллер:
func startNewGame(){
}
И его вызов во viewDidLoad
:
override func viewDidLoad() {
super.viewDidLoad()
startNewGame()
}
Теперь начнём добавлять в метод начала новой игры следующее:
textField.stringValue = ""
Очищаем текстовое поле. Заметьте, что вместо использования свойства text
мы получаем stringValue
- это старый стиль Objective-C
.
Есть также свойства для получения целочисленного значения поля
integerValue
. По логике он должен был бы возвращать опциональное значение. Но нет: он вернет число, если содержимое поля приводимо к числу, а в противном случае0
.
guesses.removeAll(keepingCapacity: true)
answer = ""
Очищаем попытки угадывания с сохранением размерности, что позволит сэкономить драгоценные ресурсы на изменение размера массива. А также очищаем ответ.
var numbers = Array(0...9)
//Создаём массив с числами от 0 до 9
numbers = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: numbers) as! [Int]
Здесь мы используем кое-что новенькое - генератор случайных чисел для того, чтобы случайным образом перемешать объекты в массиве. Заметьте, что принимает он гетерогенный массив Any
и возвращает его же. Поэтому мы специально приводим его к [Int]
.
Данный объект-генератор называется разделяемым, так как он делит свой пул с
arc4random
-генераторами C. Это значит, что обращение к одному из них влияет на результаты другого.
Помимо такого источника можно было воспользоваться другими классами-генераторами случайных чисел:
GKARC4RandomSource
- независимый, но детерминированный источник случайных данных. И хотя его поведение сходно с базовыми алгоритмами, он отделён от системного генератора, ровно как и от других генераторов данного класса. Для повторения последовательности необходимо знать зерно (свойство seed
).GKLinearCongruentialRandomSource
- линейный генератор используется для более быстрого получения чисел, при этом ослабляется элемент случайности.GKMersenneTwisterRandomSource
- более случайный генератор, но и более медленный.Учтите, что так же, как и обычные генераторы, данные генераторы неприменимы в криптографии, так как числа получаются псевдослучайными.
Можно получить из генераторов случайные числа с помощью методов протокола GKRandom
:
nextInt()
- случайное целое числоnextInt(upperBound:)
- следующее целое число от 0 (включительно) до верхней границы (невключительно)nextUniform()
- случайное число с плавающей запятойnextBool()
- случайное булево значениеИспользуя класс
GKRandomDistribution
, и его подклассыGKGaussianDistribution
иGKShuffledDistribution
, можно заключать генераторы в распределения величин, что позволит получать значения в заданном диапазоне. Также с их помощью можно симулировать игровые кости (включая заранее созданные 6-и и 20-ти гранные костиd6
иd20
).
for _ in 0..<4 {
answer.append(String(numbers.removeLast()))
}
tableView.reloadData()
Тут создаётся строка ответа путём изъятия из источника чисел.
А в конце мы перезагружаем данные таблицы для очистки от старых попыток.
Сперва реализуем число строк:
func numberOfRows(in tableView: NSTableView) -> Int {
return guesses.count
}
Создадим вспомогательную функцию, сравнивающую ответ пользователя с заданным в программе:
func result(for guess: String) -> String {
var bulls = 0
//Число полных совпадений - буква и позиция
var cows = 0
//Полусовпадения - буква есть, но не на той позиции
for (index, letter) in guess.enumerated() {
if letter == answer[index] {
bulls += 1
} else if answer.contains(letter) {
cows += 1
}
}
//Поэлементно сравниваем ответ пользователя с верным для изменения значений
return "\(bulls)b \(cows)c"
}
К сожалению, это всё работать не будет - мы получим ошибку:
'subscript' is unavailable: cannot subscript String with an Int, see the documentation comment for discussion
Это связано с тем, что можно получить доступ к элементу строки через сабскрипт только через индексы, а не целочисленные значения.
Исправим это, создав расширение типа String
:
extension String {
subscript(_ i: Int) -> Character {
return Array(self)[i]
}
}
Это не самое эффективное решение, так как содержимое копируется в массив.
Запретим выбор ячейки:
func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
return false
}
Метод для возврата ячейки несколько отличается:
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
}
Он возвращает представление для строки в столбце.
Допишем внутрь него следующее:
guard let cell = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView else {
return nil
}
Эти строки аналогичны строкам по получению реиспользуемой ячейки для iOS, однако идентификатор передаётся из колонки. А также указывается владелец файла с интерфейсом, который выполняет загрузку представления. Также здесь выполняется приведение к ячейке, так по-умолчанию будет возвращено базовое представление NSView
.
Добавим в зависимости от колонки либо значение, введённое пользователем, либо результат отгадывания (а также вернём ячейку):
if tableColumn?.title == "Guess" {
cell.textField?.stringValue = guesses[row]
} else {
cell.textField?.stringValue = result(for: guesses[row])
}
return cell
В завершение создадим внутренности метода buttonPressed
с нашей логикой по отгадыванию.
Начните с извлечения значения текстового поля в переменную guess
:
let guess = textField.stringValue
Проверьте, 4 ли символа в ней ровно и если нет, то верните управление:
Ответguard guess.count == 4 else {
return
}
Также проверьте, все ли символы отличны (это можно сделать с помощью множества), если нет - также верните управление:
Ответguard Set(guess).count == 4 else {
return
}
А теперь изучим кое-что новое - множества символов.
let badCharacters = CharacterSet(charactersIn:: "0123456789").inverted
//Этим мы создали набор символов, который включает любые символы кроме тех, что есть в указанной строке
guard guess.rangeOfCharacter(from: badCharacters) == nil else {
return
}
//Если хоть какой-то символ из строки содержится в строке, введённой пользователем, то ранний выход
Теперь мы добавим ответ в начало массива ответов, а также вставим соответствующую строчку в таблицу:
guesses.insert(guess, at: 0)
tableView.insertRows(at: IndexSet(integer: 0), withAnimation: .slideDown)
И напоследок создадим алерт в случае, если ответ содержит 4 быка:
if result(for: guess).contains("4b") {
let alert = NSAlert()
//Создаём новый алерт
alert.messageText = "You win!"
//Добавляет тайтл
alert.informativeText = "Congratulations! Click OK to play again."
//Добавляем текст информации
alert.runModal()
//Запускаем в модальном виде
//Стоит начать новую игру
startNewGame()
}
Алерты в macOS менее удобные, чем в других системах Apple - Вам приходится вручную добавлять текст в свойства вместо конструктора.
Подробнее с методами и свойствами алертов в AppKit (создание кнопок реагирования и так далее) можно ознакомиться NSAlert - AppKit | Apple Developer Documentation
Запустите приложение и поиграйте в него!
Когда Вы отгадаете правильный ответ, то появится алерт. В модальном виде Вы не можете вернуться к работе с приложением, пока не закроете алерт.