Глобальные переменные при программировании в iOS
В одном из комментариев на сайте, читатель поинтересовался каким образом следует работать с глобальными переменными. Он даже привел пример приложения, над которым он работает. Если я правильно понял, то это приложение опросник, которое по очереди задает вопросы, принимает ответы, суммирует результаты ответов и в конце выводит какой-то результат. Читатель сказал, что для каждого вопроса он использует отдельный контроллер, и соответсвенно хочет отловить ответы с каждого контроллера. Я не согласен с подобной реализацией такой задачи, но в любом случае изложу в данной статье несколько вариантов решения, которые я вижу. А также свой вариант реализации подобного опросника, при котором глобальная переменная с результатом и вовсе не понадобится.
Для начала я собрал проект, в котором я сделал четыре контроллера:
Три контроллера содержат кнопки Да и Нет и четвертый будет использоваться для вывода результата.
Не буду подробно расписывать какие идентификаторы я задавал в Storyboard, и что именно я писал в коде. В этом нет ничего сложного. Просто приведу код, контроллеров из проекта.
// // proSwift.ru // // // ViewController.swift // Questionarium // import UIKit class ViewController: UIViewController { @IBOutlet weak var qLabel: UILabel! @IBOutlet weak var yesButton: UIButton! @IBOutlet weak var noButton: UIButton! @IBAction func yesButtonTapped(_ sender: UIButton) { next() } @IBAction func noButtonTapped(_ sender: UIButton) { next() } private func next() { if self is q1ViewController { performSegue(withIdentifier: "toQ2Segue", sender: nil) } else if self is q2ViewController { performSegue(withIdentifier: "toQ3Segue", sender: nil) } else if self is q3ViewController { performSegue(withIdentifier: "toResultSegue", sender: nil) } } override func viewDidLoad() { super.viewDidLoad() configureButton(btn: yesButton) configureButton(btn: noButton) } private func configureButton(btn: UIButton) { btn.backgroundColor = UIColor(red: 127/255, green: 89/255, blue: 147/255, alpha: 1.0) btn.setTitleColor(UIColor.white, for: .normal) btn.setTitleColor(UIColor.lightGray, for: .highlighted) btn.layer.cornerRadius = 4.0 btn.layer.shadowOpacity = 0.6 btn.layer.shadowOffset = CGSize(width: 2, height: 2) btn.layer.shadowRadius = 2 } } class q1ViewController: ViewController { @IBAction func toStartSegue(sender: UIStoryboardSegue) { } override func viewDidLoad() { super.viewDidLoad() qLabel.text = "3 + 1 > 5?" } } class q2ViewController: ViewController { override func viewDidLoad() { super.viewDidLoad() qLabel.text = "4 + 5 = 9?" } } class q3ViewController: ViewController { override func viewDidLoad() { super.viewDidLoad() qLabel.text = "11 + 2 != 13?" } }
// // proSwift.ru // // // ResultViewController.swift // Questionarium // import UIKit class ResultViewController: UIViewController { @IBOutlet weak var resultLabel: UILabel! @IBOutlet weak var resetButton: UIButton! private func configureButton(btn: UIButton) { btn.backgroundColor = UIColor(red: 227/255, green: 189/255, blue: 47/255, alpha: 1.0) btn.setTitleColor(UIColor.white, for: .normal) btn.setTitleColor(UIColor.lightGray, for: .highlighted) btn.layer.cornerRadius = 4.0 btn.layer.shadowOpacity = 0.6 btn.layer.shadowOffset = CGSize(width: 2, height: 2) btn.layer.shadowRadius = 2 } override func viewDidLoad() { super.viewDidLoad() configureButton(btn: resetButton) } @IBAction func resetButtonTapped(_ sender: UIButton) { performSegue(withIdentifier: "toStartSegue", sender: self) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "toStartSegue" { } } }
Решение задачи
Итак, для подсчета результатов мы будем использовать схему при которой да = 10, а нет = 5. Это произвольные значения. Мы их определили, чтобы понимать ожидаемый результат.
Еще раз взглянем на проект: при отображении контроллеров и ответе пользователя на вопрос нам нужно запоминать результат ответов.
Вариант 1. Глобальная переменная.
Глобальную переменную можно объявить в любом файле за пределами объявления класса. Например, у нас есть файл AppDelegate, в котором задекларирован делегат приложения. Но это не значит что мы не можем ничего там писать. До объявления класса объявите переменную и константу:
// // proSwift.ru // var globalresult: Int = 0 let checkSum: Int = 20
Теперь к этой переменной и константе есть доступ из любой точки приложения. Первая будет использоваться для подсчета результатов ответов, а вторая — это сумма правильных ответов. Я понимаю что это примитив и это не совсем правильно, но у нас задача не на разработку верного алгоритма подсчета ответов.
Добавьте в ResultViewController в метод viewDidLoad() строку вывода результата на экран:
// // proSwift.ru // override func viewDidLoad() { super.viewDidLoad() configureButton(btn: resetButton) resultLabel.text = "Сумма всех ответов составила: \(globalresult) пунктов" globalresult = 0 }
И не забыть сбросить результат, для следующего круга.
Все, все работает, считает, сравнивает…
Вариант 2. Класс Singleton
На страницах нашего сайта мы не раз разбирали Singleton. В статье Синглтон на Swift, или как правильно написать Singleton и вот в этой статье: Шаблоны программирования на Swift: И снова Singleton. Давайте применим знания на практике.
Для удобства давайте добавим еще один файл в проект и назовем его Manager.swift. Внутри этого файла необходимо создать класс QuestionsManager, в котором указать нужные нам свойства: переменную результата и сумму правильных ответов. Также сделать все необходимы манипуляции, чтобы этот классицизмах являлся Singleton.
// // proSwift.ru // // // Manager.swift // Questionarium // import Foundation class QuestionsManager { var result: Int = 0 let checkSum = 20 static let sharedInstance = QuestionsManager() private init() { } func reset() { result = 0 } }
Теперь нужно добавь свойство ViewController. Т.к. наши контроллеры с вопросами наследуют от этого ViewController, то это свойство следует указать только один раз после объявления аутлетов. А также изменить обработкичики нажатия на кнопки Да и Нет.
// // proSwift.ru // let qManager = QuestionsManager.sharedInstance @IBAction func yesButtonTapped(_ sender: UIButton) { //globalresult = globalresult + 10 qManager.result += 10 next() } @IBAction func noButtonTapped(_ sender: UIButton) { //globalresult = globalresult + 5 qManager.result += 5 next() }
Также в ResultViewController нужно добавить свойство с менеджером и изменить код вывода результата на экран. А нужно изменить код сброса результатов для начала следующего круга.
// // proSwift.ru // let qManager = QuestionsManager.sharedInstance override func viewDidLoad() { super.viewDidLoad() configureButton(btn: resetButton) resultLabel.text = "Сумма всех ответов составила: \(qManager.result) пунктов. " if qManager.result == qManager.checkSum { resultLabel.text = resultLabel.text! + "Сумма пунктов соответствует сумме правильных ответов. Предположительно, вы оветили правильно на все вопросы " } qManager.reset() }
Ссылка проекта на GitHub.
Вариант 3. Все в одном контроллере
И третий вариант, который я бы хотел вам показать, это создание подобного проекта, только с использованием одного контроллера. В этом случае глобальные переменны вовсе не нужны, — все подсчеты происходят внутри одного класса контроллера. Мне придется создать новый проект, чтобы предыдущий можно было выложить целиком на GitHub.
Вот что у меня получилось:
Созданные аутлеты и экшены, а также вспомогательные методы:
// // proSwift.ru // import UIKit class ViewController: UIViewController { @IBOutlet weak var qLabel: UILabel! @IBOutlet weak var resultLabel: UILabel! @IBOutlet weak var yesButton: UIButton! @IBOutlet weak var noButton: UIButton! @IBOutlet weak var resetButton: UIButton! override func viewDidLoad() { super.viewDidLoad() configureButton(btn: yesButton) configureButton(btn: noButton) configureButton(btn: resetButton, isReset: true) } private func configureButton(btn: UIButton, isReset: Bool = false) { if isReset { btn.backgroundColor = UIColor(red: 227/255, green: 189/255, blue: 47/255, alpha: 1.0) } else { btn.backgroundColor = UIColor(red: 127/255, green: 89/255, blue: 147/255, alpha: 1.0) } btn.setTitleColor(UIColor.white, for: .normal) btn.setTitleColor(UIColor.lightGray, for: .highlighted) btn.layer.cornerRadius = 4.0 btn.layer.shadowOpacity = 0.6 btn.layer.shadowOffset = CGSize(width: 2, height: 2) btn.layer.shadowRadius = 2 } @IBAction func yesButtonTapped(_ sender: UIButton) { } @IBAction func noButtonTapped(_ sender: UIButton) { } @IBAction func resetButtonTapped(_ sender: UIButton) { } }
Итак, суть данного подхода в том, чтобы все вопросы записать в массив, и перебирая элементы массива выводить вопросы на экран. При этом заменять контроллер мы не будем. Мы будем менять надписи и прятать или наоборот выводить на экран нужные элементы.
Я предлагаю реализовать следующим образом:
// // proSwift.ru // import UIKit class ViewController: UIViewController { @IBOutlet weak var qLabel: UILabel! @IBOutlet weak var resultLabel: UILabel! @IBOutlet weak var yesButton: UIButton! @IBOutlet weak var noButton: UIButton! @IBOutlet weak var resetButton: UIButton! var questions = ["3 + 1 > 5?", "4 + 5 = 9?", "11 + 2 != 13?"] var index = 0 var result = 0 let checkSum = 20 override func viewDidLoad() { super.viewDidLoad() configureButton(btn: yesButton) configureButton(btn: noButton) configureButton(btn: resetButton, isReset: true) updateUI() } private func configureButton(btn: UIButton, isReset: Bool = false) { if isReset { btn.backgroundColor = UIColor(red: 227/255, green: 189/255, blue: 47/255, alpha: 1.0) } else { btn.backgroundColor = UIColor(red: 127/255, green: 89/255, blue: 147/255, alpha: 1.0) } btn.setTitleColor(UIColor.white, for: .normal) btn.setTitleColor(UIColor.lightGray, for: .highlighted) btn.layer.cornerRadius = 4.0 btn.layer.shadowOpacity = 0.6 btn.layer.shadowOffset = CGSize(width: 2, height: 2) btn.layer.shadowRadius = 2 } private func updateUI() { switch index { case 0,1,2: resultLabel.isHidden = true resetButton.isHidden = true qLabel.isHidden = false yesButton.isHidden = false noButton.isHidden = false qLabel.text = questions[index] case 3: resultLabel.isHidden = false resetButton.isHidden = false qLabel.isHidden = true yesButton.isHidden = true noButton.isHidden = true resultLabel.text = "Сумма всех ответов составила: \(result) пунктов. " if result == checkSum { resultLabel.text = resultLabel.text! + "Сумма пунктов соответствует сумме правильных ответов. Предположительно, вы оветили правильно на все вопросы " } result = 0 default: break } } private func next() { index += 1 updateUI() } @IBAction func yesButtonTapped(_ sender: UIButton) { result += 10 next() } @IBAction func noButtonTapped(_ sender: UIButton) { result += 5 next() } @IBAction func resetButtonTapped(_ sender: UIButton) { index = 0 updateUI() } }
Как видите, весь функционал реализован внутри только одного контроллера. Поэтому глобальные переменные вовсе не нужны. Мы используем переменные-свойства класса. Они доступны из любой точки класса контроллера.
Что еще нужно знать
Для решения поставленной вначале урока задачи можно обойтись без глобальных переменных вовсе. В других случаях, если требуется какой-либо глобальный объект, всегда используют Singleton. Использование глобальных переменных — крайне не рекомендуется по причине глобального пространства имен и впринципе неудобности такого подхода. Классы, любезно предоставленные нам Apple используют Singleton подход. Например:
// // proSwift.ru // // Swift 3 let udef = UserDefaults.standard // только одно хранилище let app = UIApplication.shared // только одно приложение let screen = UIScreen.main // только один экран у устройства let ncenter = NotificationCenter.default // только один центр уведомлений
Надеюсь я смог ответить читателю на его вопрос. Если у вас возникают вопросы по текущем тексту уроков или может есть свой вопрос или тема для изучения — пишите в комментариях — будем изучать все вместе.
Спасибо за урок!
Я не знаю прочитаете вы или нет этот комментарий, но хочу кое-что отметить)
В проекте наблюдается неограниченный рост памяти — все ViewController’ы остаются в памяти и эти ячейки памяти никогда не высвобождаются, даже если инициировать Memory Warning.
P.s. я только начал писать на Swift и это жуткая проблема для меня.
Ох, не заметил сразу ссылку на GitHub.
Обнаружил в вашем проекте такую вещь, как Unwind Segue, по которой у вас уже есть статья. Это решает мою проблему, отлично!
Спасибо еще раз 😉