#swift #mvvm #protocols
#swift #mvvm #протоколы
Вопрос:
Я рефакторингую проект для использования MVVM и использую протоколы, чтобы гарантировать, что мои модели представления имеют согласованную структуру. Это отлично работает для определения общедоступных свойств, относящихся к вводу и выводу (которые основаны на внутренних структурах), но определение действий таким же образом оказывается проблематичным, поскольку в настоящее время они определяются как замыкания, которые должны ссылаться на свойства модели просмотра. Если я использую тот же подход, что и для ввода и вывода свойств, я не думаю, что смогу получить доступ к свойствам содержащего экземпляра.
Пример:
protocol ViewModelType {
associatedtype Input
associatedtype Output
associatedtype Action
}
final class MyViewModel: ViewModelType {
struct Input { var test: String }
struct Output { var result: String }
struct Action {
lazy var createMyAction: Action<String, Void> = { ... closure to generate Action which uses a MyViewModel property }
}
var input: Input
var output: Output
var action: Action
}
Это не нарушает условия сделки, если я не могу этого сделать, но мне было любопытно, поскольку я не вижу никакого способа получить доступ к родительским свойствам.
Ответ №1:
Ответ на ваш вопрос
Давайте начнем с примечания, которое createMyAction: Action<String, Void>
ссылается на тип ( struct
) с именем Action
, как если бы он был универсальным, но вы не объявили его как таковой и, следовательно, не будет работать.
И чтобы ответить на ваш вопрос о вложенном, struct Action
может ссылаться на его внешние class MyViewModel
— да, вы можете ссылаться на static
свойства, подобные этому:
struct Foo {
struct Bar {
let biz = Foo.buz
}
static let buz = "buz"
}
let foobar = Foo.Bar()
print(foobar.biz)
Но вам, вероятно, следует избегать таких циклических ссылок. И я опущу любой уродливый взлом, который мог бы обеспечить такую циклическую ссылку на нестатические свойства (вероятно, будет включать изменяемые необязательные типы). Это запах кода.
Предложение для MVVM
Похоже, вы хотели бы объявить Action
как функцию? Я сам использую этот протокол:
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
Первоначально вдохновленный чистой архитектурой SergDort.
Вы можете подготовить экземпляр input
(содержащий Observable
классы) из UIViewController
и вызвать transform
функцию, а затем сопоставить Output
преобразования (являющиеся Observables
классами) для обновления графического интерфейса.
Таким образом, этот код предполагает, что у вас есть базовые знания о реактивности. Что касается Observable
s, вы можете выбирать между RxSwift или ReactiveSwift — да, их имена похожи.
Если вас устраивает Rx, это отличный способ добиться хорошей архитектуры MVVM с помощью простых асинхронных обновлений графического интерфейса. В приведенном ниже примере вы найдете тип, Driver
который задокументирован здесь, но краткое объяснение заключается в том, что это то, что вы хотите использовать для ввода из представлений и ввода в представления, поскольку он обновляет представления в потоке GUI и гарантированно не выдает ошибку.
Чистая архитектура содержит, например PostsViewModel
:
final class PostsViewModel: ViewModelType {
struct Input {
let trigger: Driver<Void>
let createPostTrigger: Driver<Void>
let selection: Driver<IndexPath>
}
struct Output {
let fetching: Driver<Bool>
let posts: Driver<[PostItemViewModel]>
let createPost: Driver<Void>
let selectedPost: Driver<Post>
let error: Driver<Error>
}
private let useCase: PostsUseCase
private let navigator: PostsNavigator
init(useCase: PostsUseCase, navigator: PostsNavigator) {
self.useCase = useCase
self.navigator = navigator
}
func transform(input: Input) -> Output {
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
let posts = input.trigger.flatMapLatest {
return self.useCase.posts()
.trackActivity(activityIndicator)
.trackError(errorTracker)
.asDriverOnErrorJustComplete()
.map { $0.map { PostItemViewModel(with: $0) } }
}
let fetching = activityIndicator.asDriver()
let errors = errorTracker.asDriver()
let selectedPost = input.selection
.withLatestFrom(posts) { (indexPath, posts) -> Post in
return posts[indexPath.row].post
}
.do(onNext: navigator.toPost)
let createPost = input.createPostTrigger
.do(onNext: navigator.toCreatePost)
return Output(fetching: fetching,
posts: posts,
createPost: createPost,
selectedPost: selectedPost,
error: errors)
}
}
Комментарии:
1. Я «взломал» его, создав структуру (называемую Actions), содержащую общедоступные методы получения для частных ленивых переменных, которые определяли действия RxSwift, которые я искал. Это работает, но я чувствовал себя некомфортно, делая это, что, вероятно, было моим подсознанием, предупреждающим меня о запахе кода. Я думаю, проблема заключалась в том, что я пытался рассматривать действия как отдельную сущность в своем собственном праве, а не как выходные данные виртуальной машины (что, конечно, так и есть). Спасибо за указатель — я проведу рефакторинг и посмотрю, устранит ли это другую проблему, с которой я только что столкнулся…
2. дайте мне знать, если вам нужны дополнительные данные. Вы могли бы взглянуть на это приложение для кошелька iOS для криптовалюты Zilliqa под названием Zhip, которое я написал с открытым исходным кодом. Zhip использует этот
transform(input) -> Output
шаблон в своих ViewModels. github.com/OpenZesame/Zhip