Začneme výberom vhodnej architektúry pre iOS app, keďže tá nam definuje, ako si všetky komponenty appky zorganizujeme a ako budú spolu komunikovať. Existuje niekoľko rôznych architektonických vzorov, ktoré sa používajú pri vývoji iOS appiek. Tu sú niektoré z najbežnejších:
Model-View-Controller (MVC)
Je to jeden z najbežnejších architektonických vzorov pre iOS appky. V tejto architektúre sú komponenty aplikácie rozdelené do troch hlavných častí: Model (údaje), View (užívateľské rozhranie) a Controller (riadiaca logika). Model obsahuje dáta a biznis logiku, View zobrazuje užívateľské rozhranie a Controller riadi interakciu medzi Modelom a View. Táto architektúra sa prezýva aj Massive View Controller 🙂 a to z toho dôvodu, že Controller obsahuje celú logiku, ktorý tvorí podstatnú časť aplikácie. Model a View sú prevažne jednoduché.
import UIKit
// Model
struct User {
var username: String
}
// View
class UserViewController: UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
var user: User?
override func viewDidLoad() {
super.viewDidLoad()
updateView()
}
func updateView() {
if let user = user {
usernameTextField.text = user.username
}
}
@IBAction func changeUsername(_ sender: Any) {
user?.username = usernameTextField.text ?? ""
updateView()
}
}
// Controller
class UserController: UIViewController {
var user: User = User(username: "John")
var userViewController: UserViewController?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let userViewController = segue.destination as? UserViewController {
self.userViewController = userViewController
userViewController.user = user
}
}
}
Model-View-ViewModel (MVVM)
Model reprezentuje dáta a biznis logiku, ktorá reprezentuje údaje iOS appky a zodpovedá za ich manipuláciu. View zobrazuje užívateľské rozhranie – môže to byť obrazovka, tlačidlo, textové pole alebo hociktorý iný vizuálny prvok, s ktorým užívateľ interaguje. ViewModel je medzičlánok medzi View a Modelom. Obsahuje logiku, ktorá riadi zobrazovanie dát z Modelu vo View a tiež reakcie na užívateľské akcie z View. ViewModel by mal byť nezávislý na View a obsahuje iba tie dáta a metódy, ktoré sú potrebné na zobrazenie informácií v View. Vďaka tomuto rozdeleniu môže byť ViewModel prepoužívaný s viacerými Views a jedna obrazovka môže byť zložená z viacerých menších Views a ViewModelov, čo zjednodušuje jej komplexitu.
import SwiftUI
// Model
struct User {
var username: String
}
// ViewModel
class UserViewModel: ObservableObject {
@Published var user: User
init(user: User) {
self.user = user
}
func changeUsername(newUsername: String) {
user.username = newUsername
}
}
// View
struct UserView: View {
@ObservedObject var viewModel: UserViewModel
var body: some View {
VStack {
TextField("Enter username", text: $viewModel.user.username)
.padding()
Button(action: {
viewModel.changeUsername(newUsername: "New Username")
}) {
Text("Change Username")
}
}
}
}
// Usage
struct ContentView: View {
@StateObject var userViewModel = UserViewModel(user: User(username: "John"))
var body: some View {
UserView(viewModel: userViewModel)
}
}
Model-View-Presenter (MVP)
Tento vzor je podobný MVC, ale Controller je nahradený Presenterom. Presenter spracúva vstupy od užívateľa a aktualizuje Model aj View.
import UIKit
// Model
struct User {
var username: String
}
// View
class UserViewController: UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
var presenter: UserPresenter!
override func viewDidLoad() {
super.viewDidLoad()
presenter = UserPresenter(view: self)
presenter.viewDidLoad()
}
@IBAction func changeUsername(_ sender: Any) {
presenter.changeUsername(newUsername: usernameTextField.text ?? "")
}
}
extension UserViewController: UserView {
func displayUser(user: User) {
usernameTextField.text = user.username
}
}
// Presenter
protocol UserView: class {
func displayUser(user: User)
}
class UserPresenter {
weak var view: UserView?
var user: User = User(username: "John")
init(view: UserView) {
self.view = view
}
func viewDidLoad() {
view?.displayUser(user: user)
}
func changeUsername(newUsername: String) {
user.username = newUsername
view?.displayUser(user: user)
}
}
Model-View-Intent (MVI)
Tento vzor je populárny pri použití s architektúrou unidirectional data flow. Intents (zámery) reprezentujú akcie, ktoré sa majú vykonať, a zodpovedajú za aktualizáciu Modelu.
import SwiftUI
import Combine
// Model
struct User {
var username: String
}
// Intent
enum UserIntent {
case changeUsername(String)
}
// View
struct UserView: View {
@State private var username: String = ""
private var userPublisher = PassthroughSubject()
var body: some View {
VStack {
TextField("Enter username", text: $username)
.padding()
Button(action: {
userPublisher.send(User(username: self.username))
}) {
Text("Change Username")
}
}
.onReceive(userPublisher) { user in
// Process user change
print("New username: \(user.username)")
}
}
}
// Usage
struct ContentView: View {
var body: some View {
UserView()
}
}
Clean Architecture
Tento architektonický vzor oddeľuje rôzne časti aplikácie do vrstiev, čo umožňuje lepšiu modularitu a testovateľnosť. Základnými časťami sú Domain Layer (obsahuje biznis logiku a entitné triedy), Data Layer (komunikuje s externými zdrojmi dát) a Presentation Layer (obsahuje užívateľské rozhranie a riadiacu logiku).
Domain Layer
// Model
struct User {
var id: String
var username: String
}
// UserRepository protocol
protocol UserRepository {
func fetchUser(completion: @escaping (Result) -> Void)
}
// UseCase
class GetUserUseCase {
let userRepository: UserRepository
init(userRepository: UserRepository) {
self.userRepository = userRepository
}
func execute(completion: @escaping (Result) -> Void) {
userRepository.fetchUser(completion: completion)
}
}
Data Layer
// UserDataRepository
class UserDataRepository: UserRepository {
func fetchUser(completion: @escaping (Result) -> Void) {
// Fetch user data from API or database
// Completion handler returns User object or error
}
}
Presentation Layer
import UIKit
class UserViewController: UIViewController {
let getUserUseCase = GetUserUseCase(userRepository: UserDataRepository())
override func viewDidLoad() {
super.viewDidLoad()
getUserUseCase.execute { result in
switch result {
case .success(let user):
print("User: \(user)")
// Update UI with user data
case .failure(let error):
print("Error fetching user: \(error)")
}
}
}
}
The Composable Architecture
Konečne TCA. The Composable Architecture (TCA) je moderný architektonický vzor, ktorý je populárny najmä v kombinácii s jazykom Swift a frameworkom SwiftUI. TCA je navrhnutý tak, aby zjednodušil vývoj iOS appiek a zabezpečil ich testovateľnosť a znovupoužiteľnosť.
Základnými časťami The Composable Architecture sú:
- State: Stav aplikácie je reprezentovaný štruktúrou dát, ktorá obsahuje všetky informácie potrebné na vykreslenie užívateľského rozhrania.
- Reducer: Reducer je funkcia, ktorá prijíma aktuálny stav, akciu a vracia nový stav. Táto funkcia definuje, ako sa aplikácia mení na základe užívateľských akcií.
- Action: Akcia reprezentuje udalosť alebo požiadavku od užívateľa alebo z iných častí aplikácie. Typicky sa definuje ako enum s príslušnými prípadmi pre každú možnú akciu.
- Store: Store je centrálna komponenta, ktorá drží aktuálny stav aplikácie, spravuje spúšťanie akcií a upozorňuje výstupné komponenty (napr. View) na zmeny stavu.
import ComposableArchitecture
import SwiftUI
@main
struct TCAApp: App {
var body: some Scene {
WindowGroup {
DetailView(
store: Store(initialState: Detail.State(username: "")) {
Detail()
}
)
}
}
}
struct Detail: Reducer {
struct State: Equatable {
var username: String
}
enum Action: Equatable {
case updateUsername(String)
case save
}
var body: some Reducer {
Reduce { state, action in
switch action {
case let .updateUsername(username):
state.username = username
return .none
case .save:
print("Save: \(state.username)")
return .none
}
}
}
}
struct DetailView: View {
let store: StoreOf
var body: some View {
WithViewStore(store, observe: { $0 }, content: { viewStore in
VStack(spacing: 20) {
TextField(
"Username",
text: viewStore.binding(get: { $0.username }, send: Detail.Action.updateUsername)
)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Save") {
viewStore.send(.save)
}
.buttonStyle(.plain)
}
.padding(.horizontal, 20)
})
}
}
Easy, že? Vrelo odporúčam si ešte prebehnúť README a Examples na Githube a tutorial od tvorcov Brandona Williamsa a Stephena Celisa.
A prečo práve TCA?
Lebo zaisťuje jednoduchosť a čistotu kódu. TCA poskytuje jednoduchý a čistý spôsob, ako štruktúrovať aplikáciu. Reducer poskytuje jasný a jednoduchý spôsob, ako definovať, ako sa má stav aplikácie meniť v závislosti od akcií. Jedna z najväčších výhod TCA je, že oddeľuje biznis logiku od ostatných častí aplikácie, čo robí testovanie jednoduchým a efektívnym. TCA používa jednosmerný tok dát (unidirectional data flow), čo zjednodušuje sledovanie toho, ako sa mení stav aplikácie v závislosti od užívateľských akcií. Toto znižuje počet chýb spojených s asynchrónnymi udalosťami a nečakaným správaním. Zjednodušuje správu stavu – používa centrálne úložisko (Store), ktoré riadi stav aplikácie a spravuje spúšťanie akcií. To zjednodušuje správu stavu a umožňuje jednoduchý prístup k stavu aplikácie z ľubovoľnej časti aplikácie. A navyše je plne kompatibilné so SwiftUI.
iOS app: Kľúč k oslovovaniu širokej základne užívateľov
Vytváranie iOS appiek je kľúčom k oslovovaniu širokej základne užívateľov a poskytuje príležitosť pre rast a rozvoj. Pre tých, ktorí nemajú potrebné zručnosti, je tu možnosť využiť služby profesionálnych vývojárov ako sú tí naši vo Verteco, ktorí im pomôžu realizovať ich nápady bez nutnosti naučiť sa nové technológie. Týmto spôsobom sa zabezpečuje rýchle a efektívne vytvorenie kvalitnej aplikácie.