ظرف حالت مانند Redux در SwiftUI: اتصالات.

در ماه گذشته، قدردانی عمیقی از مزایای داشتن یک منبع حقیقت واحد و یک ظرف حالت برای مدیریت کل وضعیت برنامه در یک مکان واحد به دست آوردم. من قبلاً این استراتژی را در چند برنامه قبلی خود اجرا کرده ام و قصد دارم به استفاده از آن در تمام پروژه های آینده ادامه دهم.
import Foundation
struct AppState: Equatable {
var showsById: [Ids: Show] = [:]
var seasons: [Ids: Season] = [:]
var episodes: [Ids: Episode] = [:]
var showImages: [Ids: FanartImages] = [:]
var watchedHistory: [Ids] = []
}
enum AppAction: Equatable {
case markAsWatched(episode: Ids, watched: Bool)
}
import SwiftUI
import KingfisherSwiftUI
struct HistoryView: View {
@ObservedObject var store: Store<AppState, AppAction>
var body: some View {
LazyVGrid(columns: [.init(), .init()]) {
ForEach(store.state.watchedHistory, id: \.self) { id in
posterView(for: id)
}
}
}
private func posterView(for id: Ids) -> some View {
let image = store.state.showImages[id]?.tvPosters?.first?.url
let episode = store.state.episodes[id]
let title = episode?.title ?? ""
let date = episode?.firstAired ?? Date()
return VStack {
image.map {
KFImage($0)
}
Text(title)
Text(verbatim: DateFormatter.shortDate.string(for: date))
.foregroundColor(.secondary)
.font(.subheadline)
}
}
}
مسئله اصلی در اینجا منطق قالب بندی است که در داخل نما وجود دارد. ما نمیتوانیم آن را با استفاده از تستهای واحد تأیید کنیم. مشکل دیگر پیش نمایش SwiftUI است. ما باید کل فروشگاه را با کل وضعیت برنامه ارائه کنیم تا یک صفحه نمایش واحد ارائه شود. و ما نمی توانیم نمای را در یک بسته سوئیفت جدا نگه داریم زیرا به کل وضعیت برنامه بستگی دارد.
ما میتوانیم با استفاده از فروشگاههای مشتق شده که فقط بخش مورد نیاز حالت برنامه را ارائه میکنند، وضعیت را کمی بهبود ببخشیم. اما هنوز باید منطق قالب بندی را در جایی خارج از نما نگه داریم.
اجازه دهید مؤلفه دیگری را معرفی کنم که بین کل فروشگاه برنامه و نمای اختصاصی زندگی می کند. مسئولیت اصلی این مؤلفه تبدیل وضعیت برنامه به حالت مشاهده است. من آن را Connector می نامم و جزء الهام گرفته از Redux است.
protocol Connector {
associatedtype State
associatedtype Action
associatedtype ViewState: Equatable
associatedtype ViewAction: Equatable
func connect(state: State) -> ViewState
func connect(action: ViewAction) -> Action
}
extension Store {
func connect<C: Connector>(
using connector: C
) -> Store<C.ViewState, C.ViewAction> where C.State == State, C.Action == Action {
derived(
deriveState: connector.connect(state: ),
embedAction: connector.connect(action: )
)
}
}
Connector
یک پروتکل ساده است که دو تابع را تعریف می کند. اولی کل حالت برنامه را به حالت view تبدیل می کند و دومی اقدامات view را به اقدامات برنامه تبدیل می کند. بیایید دیدگاه خود را با معرفی عملکردهای view و view اصلاح کنیم.
extension HistoryView {
struct State: Equatable {
let posters: [Poster]
struct Poster: Hashable {
let ids: Ids
let imageURL: URL?
let title: String
let subtitle: String
}
}
enum Action: Equatable {
case markAsWatched(episode: Ids)
}
typealias ViewModel = Store<State, Action>
}
ما یک مدل کاملا متفاوت برای نمای خود ایجاد می کنیم که تنها داده های مورد نیاز را در خود نگه می دارد. حالت view در اینجا یک نگاشت مستقیم از نمایش نمای و مدل آن است. view action enum تنها اقدامی است که برای این نمای خاص موجود است. شما حوادثی را که در آنها اقدامات نامرتبط می نامید، حذف می کنید. در نهایت، نمای شما کاملاً مستقل است که به شما امکان می دهد آن را در یک بسته سوئیفت جداگانه استخراج کنید.
import KingfisherSwiftUI
import SwiftUI
struct HistoryView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
LazyVGrid(columns: [.init(), .init()]) {
ForEach(viewModel.state.posters, id: \.title) { poster in
VStack {
poster.imageURL.map {
KFImage($0)
}
Text(poster.title)
Text(poster.subtitle)
}.onTapGesture {
viewModel.send(.markAsWatched(episode: poster.ids))
}
}
}
}
}
یکی دیگر از مزایای اینجا است نمای ساده. هیچ کاری نمی کند. نمایش داده های قالب بندی شده را نمایش می دهد و اقدامات را ارسال می کند. شما می توانید به سرعت به تعداد مورد نیاز پیش نمایش SwiftUI بنویسید تا همه موارد مختلف مانند بارگیری، خالی و غیره را پوشش دهید.
extension Store {
static func stub(with state: State) -> Store {
Store(
initialState: state,
reducer: .init { _, _, _ in Empty().eraseToAnyPublisher() },
environment: ()
)
}
}
struct HistoryView_Previews: PreviewProvider {
static var previews: some View {
HistoryView(
viewModel: .stub(
with: .init(
posters: [
.init(
ids: Ids(trakt: 1),
imageURL: URL(
staticString: "https://domain.com/image.jpg"
),
title: "Film",
subtitle: "Science"
)
]
)
)
)
}
}
زمان ایجاد نوع اتصال دهنده خاص است که از آن برای متصل کردن حالت برنامه به حالت view استفاده می کنیم.
enum Connectors {}
extension Connectors {
struct WatchedHistoryConnector: Connector {
func connect(state: AppState) -> HistoryView.State {
.init(
posters: state.watchedHistory.compactMap { ids in
let episode = state.episodes[ids]
return HistoryView.State.Poster(
ids: ids,
imageURL: state.showImages[ids]?.tvPosters?.first?.url,
title: episode?.title ?? "",
subtitle: DateFormatter.shortDate.string(for: episode?.firstAired) ?? ""
)
}
)
}
func connect(action: HistoryView.Action) -> AppAction {
switch action {
case let .markAsWatched(episode):
return AppAction.markAsWatched(episode: episode, watched: true)
}
}
}
}
همانطور که در مثال بالا می بینید، WatchedHistoryConnector
یک نوع مقدار ساده است که می توانیم به سرعت با استفاده از تست واحد آن را آزمایش کنیم. اکنون، باید نگاهی به نحوه استفاده از انواع کانکتور خود بیندازیم. معمولاً من کانتینر یا نماهای جریانی دارم که نماها را به فروشگاه متصل می کند.
import SwiftUI
struct RootContainerView: View {
@EnvironmentObject var store: Store<AppState, AppAction>
var body: some View {
HistoryView(
viewModel: store.connect(using: Connectors.WatchedHistoryConnector())
)
}
}
مخاطب
من تمرکز واضحی بر زمان عرضه به بازار دارم و بدهی فنی را در اولویت قرار نمی دهم. و من در فعالیت های پیش فروش/RFX به عنوان معمار سیستم، تلاش های ارزیابی برای موبایل (iOS-Swift، Android-Kotlin)، Frontend (React-TypeScript) و Backend (NodeJS-.NET-PHP-Kafka-SQL) شرکت کردم. -NoSQL). و همچنین کار پیش فروش را به عنوان یک مدیر ارشد فناوری از فرصت تا پیشنهاد از طریق انتقال دانش به تحویل موفق تشکیل دادم.
🛩️ #استارتاپ ها #مدیریت #cto #swift #typescript #پایگاه داده
📧 ایمیل: sergey.leschev@gmail.com
👋 لینکدین: https://www.linkedin.com/in/sergeyleschev/
👋 LeetCode: https://leetcode.com/sergeyleschev/
👋 توییتر: https://twitter.com/sergeyleschev
👋 Github: https://github.com/sergeyleschev
🌎 وب سایت: https://sergeyleschev.github.io