İçeriğe geç

SwiftUI ile MVVM kullanımı

Merhabalar bu yazımda SwiftUI ile MVVM tasarım deseninin birlikte kullanımını sizlerle paylaşacağım.

Projeye başlamadan önce yapılması gereken en önemli eylemlerden biri yazılım tasarım desenini belirlemek olmalıdır. Kaliteli ve okunaklı yazılmış kod, temeli sağlam bir binaya benzer. Düzgün yazılmamış proje ise gelecekte büyük sorunlar doğurur. Öncesinde vereceğiniz efor size gelecekte yapacağınız update işlemlerinde kolaylık sağlar. Bu yüzden projeye uygun doğru pattern seçmek önemlidir.

SwiftUI yapısı ile birlikte kullanılabilecek belli tasarım desenleri vardır. Örneğin Redux style pattern, Clean Swift, MVVM, View State MVVM, ModelView gibi. Bu tasarım desenlerinin kendilerine göre artıları ve eksileri var. Çok kompleks ve karmaşık desenler demek değildir ki en doğru olan tasarım deseni. Projenizin büyüklüğüne göre bu seçimi yapmak doğru olacaktır.

SwiftUI ile zaman geçirdiğimden beri projelerimde MVVM kullanmayı tercih etmekteyim. Bunun  nedeni Business Logic ile View kısmını bir birinden güzel bir şekilde ayırması ve okunaklı, basit bir pattern olması. Çoğunlukla piyasada gördüğüm projelerin büyük çoğunluğu MVVM ile yazılmış. Birde bunu Combine ile harmanlarsanız oldukça güzel kodlar ortaya çıkıyor.

Aşağıda yazdığım örnekte State tutarak View kısmınıda daha okunaklı ve verimli hale getirebiliriz. Böylelikle Error handling ve benzeri durumları yönetmek daha kolay olur.

struct ContentView: View {
    
    @StateObject var VM = ContentViewModel()
    
    var body: some View {
        content(VM.state)
            .onAppear {
                VM.loadList()
            }
    }
    
    @ViewBuilder
    private func content(_ state: ContentViewState) -> some View {
        switch state {
        case .loading:
            ProgressView()
                .progressViewStyle(CircularProgressViewStyle())
        case .error(let message):
            Spacer().alert(isPresented: .constant(true), content: {
                Alert(title: Text("Oops"), message: Text(message), dismissButton: nil)
            })
        case .content(let todoList):
            NavigationView {
                List {
                    ForEach(todoList, id: \.self) { item in
                        RowView(item: item)
                            .padding()
                    }
                }
                .navigationTitle("Todos List")
            }
        }
    }
}

struct RowView: View {
    
    var item: TodosModel
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Id: \(item.userId)")
            Text("Title: \(item.title)")
        }
        .foregroundColor(.black)
        .font(.body)
    }
}
class ContentViewModel: ObservableObject {
    
    @Published var state: ContentViewState = .loading
    
    private let todoRepo = TodosRepository()
    private var cancellables = Set<AnyCancellable>()
        
    func loadList() {
        self.state = .loading
        todoRepo.getTodos()
            .sink { error in
                switch error {
                case .finished:
                    break
                case .failure(let error):
                    self.state = .error(message: error.message)
                }
            } receiveValue: { items in
                self.state = .content(list: items)
            }
            .store(in: &cancellables)
    }
    
    deinit {
        cancellables.removeAll()
    }
}
final class TodosRepository {
    
    private let session: URLSession
    
    init(session: URLSession = .shared) {
        self.session = session
    }
    
    func getTodos() -> AnyPublisher<[TodosModel], FailureReason> {
        let request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos")!)
        return session.dataTaskPublisher(for:  request)
            .map { $0.data }
            .decode(type: [TodosModel].self, decoder: JSONDecoder())
            .mapError({ error in
                switch error {
                case is Swift.DecodingError: return .decodingFailed
                case let urlError as URLError: return .sessionFailed(error: urlError)
                default: return .other(error)
                }
            })
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}
struct TodosModel: Codable, Identifiable, Hashable {
    var userId: Int
    var id: Int
    var title: String = ""
    var completed: Bool = false
}

enum ContentViewState {
    case loading
    case content(list: [TodosModel])
    case error(message: String)
}

extension FailureReason {
    var message: String {
        switch self {
        case .decodingFailed: return "Decoding Error"
        case .sessionFailed(error: let uError): return "Url Error: \(uError.localizedDescription)"
        case .other(let error): return "Problem \(error.localizedDescription)"
        }
    }
}

enum FailureReason : Error {
    case sessionFailed(error: URLError)
    case decodingFailed
    case other(Error)
}

 

Tarih:SwiftUI

İlk Yorumu Siz Yapın

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Copyright © 2021 Kenan Atmaca