Хорошая стратегия для замены частей функциональности в iOS ViewControllers

У меня есть ВК в приложении для iOS, которые имеют довольно много элементов управления пользовательским интерфейсом. Теперь мне нужно заменить или "смоделировать" некоторые из этих элементов управления, когда они находятся в определенном состоянии. В некоторых случаях это будет просто отключение действий кнопок, но в некоторых случаях выполняемые действия должны быть заменены чем-то совершенно другим.

Мне не очень нравится идея засорять подобные проверки по всей базе кода.

if condition {
  ...Special/disabled functionality
} else {
  ...Normal functionality
}

В Android я могу просто создать подкласс для каждого фрагмента / действия и создать там функциональность, а затем выполнить if / else при вставке фрагментов или запуске действий.

Но на iOS с Storyboards/IBActions и Segues, пользовательские интерфейсы и виртуальные каналы действительно тесно связаны. Вы либо заканчиваете тем, что дублируете представления пользовательского интерфейса, либо добавляете много привередливого кода к уже большим VC.

Что было бы лучшим способом справиться с этим в iOS?

Пример кода того, чего я хочу избежать:

//Before:
class SomeViewController : UIViewController {
  @IBAction onSomeButton() {
    checkSomeState()
    doANetworkRequest(() -> {
       someCompletionHandler()
       updatesTheUI()
    }
    updateTheUIWhileLoading()
  }

  @IBAction onSomeOtherButton() {
    checkAnotherState()
    updateUI()
  }
}
//After:
class SomeViewController : UIViewController {
  @IBAction onSomeButton() {
    if specialState {
      doSomethingSimpler()
    } else {
      checkSomeState()
      doANetworkRequest(() -> {
         someCompletionHandler()
         updatesTheUI()
      }
      updateTheUIWhileLoading()
    }
  }

  @IBAction onSomeOtherButton() {
    if specialState {
      return // Do nothing
    } else {
      checkAnotherState()
      updateUI()
    }
  }
}

1 ответ

Решение

Я бы предложил использовать шаблон MVVM (Model - View - ViewModel). Вы передаете ViewModel к вашему контроллеру и делегировать все действия ему. Вы также можете использовать его, чтобы стилизовать ваши представления и решить, должны ли некоторые из них быть скрыты или отключены, и т. Д.

Давайте представим себе приложение для покупок, в котором ваши профессиональные пользователи получают скидку 10% и могут использовать бесплатную доставку.

protocol PaymentScreenViewModelProtocol {
    var regularPriceString: String { get }
    var discountedPriceString: String? { get }
    var isFreeShippingAvailable: Bool { get }

    func userSelectedFreeShipping()
    func buy()
}

class StandardUserPaymentScreenViewModel: PaymentScreenViewModelProtocol {
    let regularPriceString: String = "20"
    let discountedPriceString: String? = nil
    let isFreeShippingAvailable: Bool = false

    func userSelectedFreeShipping() {
        // standard users cannot use free shipping!
    }

    func buy() {
        // process buying
    }
}

class ProUserPaymentScreenViewModel: PaymentScreenViewModelProtocol {
    let regularPriceString: String = "20"
    let discountedPriceString: String? = "18"
    let isFreeShippingAvailable: Bool = true

    func userSelectedFreeShipping() {
        // process selection of free shipping
    }

    func buy() {
        // process buying
    }
}

class PaymentViewController: UIViewController {

    @IBOutlet weak var priceLabel: UILabel!
    @IBOutlet weak var discountedPriceLabel: UILabel!
    @IBOutlet weak var freeShippingButton: UIButton!

    var viewModel: PaymentScreenViewModelProtocol

    override func viewDidLoad() {
        super.viewDidLoad()

        priceLabel.text = viewModel.regularPriceString
        discountedPriceLabel.text = viewModel.discountedPriceString
        freeShippingButton.isHidden = !viewModel.isFreeShippingAvailable
    }

    @IBAction func userDidPressFreeShippingButton() {
        viewModel.userSelectedFreeShipping()
    }

    @IBAction func userDidPressBuy() {
        viewModel.buy()
    }
}

Этот подход позволяет вам отделить вашу логику от ваших взглядов. Так же проще проверить эту логику.
Одна вещь, которую нужно рассмотреть и решить, - это подход к внедрению модели представления в контроллер представления. Я вижу три возможности:

  1. С помощью init - вы предоставляете собственный инициализатор, требующий передачи модели представления. Это будет означать, что вы не сможете использовать segueили storyboards (вы сможете использовать xibс). Это позволит вашей модели представления быть необязательной.
  2. Посредством установки свойства с реализацией по умолчанию - если вы предоставляете некоторую форму реализации по умолчанию / пустой реализации вашей модели представления, вы можете использовать ее в качестве значения по умолчанию для нее и установить правильную реализацию позже (например, в prepareForSegue). Это позволяет вам использовать segues, storyboards и иметь модель представления не обязательно (это просто добавляет накладные расходы на наличие дополнительной пустой реализации).
  3. Через настройку свойства без реализации по умолчанию - это в основном означает, что ваша модель представления должна быть необязательной, и вам придется проверять ее почти каждый раз, когда вы получаете к ней доступ.
Другие вопросы по тегам