Глобальные переменные при программировании в 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
    // только один центр уведомлений

Надеюсь я смог ответить читателю на его вопрос. Если у вас возникают вопросы по текущем тексту уроков или может есть свой вопрос или тема для изучения — пишите в комментариях — будем изучать все вместе.




2 Comments on “Глобальные переменные при программировании в iOS

  1. Спасибо за урок!
    Я не знаю прочитаете вы или нет этот комментарий, но хочу кое-что отметить)

    В проекте наблюдается неограниченный рост памяти — все ViewController’ы остаются в памяти и эти ячейки памяти никогда не высвобождаются, даже если инициировать Memory Warning.

    P.s. я только начал писать на Swift и это жуткая проблема для меня.

    • Ох, не заметил сразу ссылку на GitHub.
      Обнаружил в вашем проекте такую вещь, как Unwind Segue, по которой у вас уже есть статья. Это решает мою проблему, отлично!

      Спасибо еще раз 😉

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

*

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.