Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 4 months ago.
Improve this question
I have been been reading a lot of about SwiftUI architecture. I read about MVVM, TCA, MVC etc. I am writing an app, where I have to get data from the JSON API and display it on the screen. I am following Apple's code samples and here is what I am doing.
For that I created a NetworkModel.
class NetworkModel {
func getPosts() async throws -> [Post] {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://jsonplaceholder.typicode.com/posts")!)
let posts = try JSONDecoder().decode([Post].self, from: data)
return posts
}
}
Post.swift
struct Post: Decodable {
let id: Int
let title: String
let body: String
}
PostListView
struct PostListView: View {
#State private var posts: [Post] = []
let networkModel = NetworkModel()
var body: some View {
List(posts, id: \.id) { post in
Text(post.title)
}.task {
do {
await networkModel.getPosts()
} catch {
print(error)
}
}
}
}
Everything works. Is this the right approach? I come from React background so in React if there is a component that is using state and that state will only be used for that component then we simply use private/local state.
I did not create any View Models etc for this app. Are VM's going to benefit me in someway?
The following example code is how I would approach using a NetworkModel.
Caveat, without dealing with the errors.
It creates a NetworkModel as a ObservableObject, which will make it easier
to update the data and have the UI refreshed automatically. The code uses
only one source of truth, the #StateObject var networkModel = NetworkModel(),
and nothing else. This, I believe is important for an architecture.
Is this the right approach?, well that is up to you to determine, by understanding
the various info you read, and your own experience.
struct Post: Decodable, Identifiable {
let userId: Int
let id: Int
let title: String
let body: String
}
#MainActor
class NetworkModel: ObservableObject {
#Published var posts: [Post] = []
func getPosts() async throws {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {return}
do {
let (data, _) = try await URLSession.shared.data(from: url)
self.posts = try JSONDecoder().decode([Post].self, from: data)
} catch {
print(error)
}
}
}
struct ContentView: View {
#StateObject var networkModel = NetworkModel()
var body: some View {
List (networkModel.posts) { post in
Text(post.title)
}
.task {
do {
try await networkModel.getPosts()
} catch {
print(error)
}
}
}
}
The info at the following link, gives some good explanations about how to manage data in your app and how SwiftUI has already a lot of structure/architecture built in to help you:
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
SwiftUI is an architecture in itself, it's best to just learn it. E.g. learn the View struct, learn how body func is called if a let property or an #State or #Binding changes. It's really quite simple.
Related
I have content that is loaded in the background.
Loading is a long running task and I want to show data as soon as it's available.
My view show loaded content and a ProgressView whenever there is more content to be expected.
struct MyListView : View
{
#MyContent
var content: [MyItem]
var body: some View
{
List
{
//...show content elements
if content.hasMoreData()
{
ProgressView().task
{
await _content.load(.end)
}
}
}
}
}
I use a custom propertyWrapper to load the data.
#propertyWrapper
struct MyContent<E> : DynamicProperty
{
final class Container<E> : ObservableObject
{
var wrappedValue : [E]
}
let container: Container<E>
var wrappedValue : [E] { container.wrappedValue }
func load() async
{
//...load more content
}
}
When the view loads, the ProgressView spins and the load function is called.
After more data has been loaded the view is refreshed. Unfortunately however the task on the ProgressView is not renewed. The load function is not called again.
I also tried wrapping the MyContent wrapper in an ObservableObject but with similar effects.
final class MyBox<E> : ObservableObject
{
#MyContent
var content: [MyItem]
func load() async
{
await _content.load(position)
await send()
}
#MainActor
private func send() async
{
objectWillChange.send()
}
}
If I look at FetchRequest which is a struct and which has a batchLimit, I think it should not be necessary to use MyBox or an ObservableObject' just to trigger and additional load` call.
How can I force the ProgressView to run the task again?
I am new to SwiftUI and programming in general. I am trying to pass data and create navigation between different views in my app.
For my data model, I am using MVVM format even though my data is entirely static right now. I have two data models that I am trying to connect via enumeration: CategoryModel and SubCategoryModel (see below).
CategoryModel:
import Foundation
import SwiftUI
enum Category: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
case explicit = "Explicit"
case adventure = "Adventure"
case artists = "Artists"
case holidays = "Holidays"
case movies = "Movies"
case people = "People"
case tasks = "Tasks"
case feelings = "Feelings"
case lifestyle = "Lifesytle"
case party = "Party"
case sports = "Sports"
}
//Root data -> set up data structure
//make Identifiable for ForEach looping in other views
struct CategoryModel: Identifiable {
let id = UUID().uuidString
let category: Category
let categoryTitle: String
let description: String
let isPurchase: Bool
let categoryImage: Image
}
class CategoryViewModel: ObservableObject {
#Published var gameCategories: [CategoryModel] = CategoryModel.all
#Published var filteredPurchase: [CategoryModel] = []
init() {
filterByPurchase()
}
func filterByPurchase() {
filteredPurchase = gameCategories.filter({ $0.isPurchase })
}
}
extension CategoryModel {
static let all = [
CategoryModel(
category: .explicit,
categoryTitle: "Adults Only",
description: "For those spicy, intimate moments.",
isPurchase: true,
categoryImage: Image(uiImage: #imageLiteral(resourceName: "Explicit"))
),
CategoryModel(
category: .adventure,
categoryTitle: "Call of the Wild",
description: "[Insert description here]",
isPurchase: false,
categoryImage: Image(uiImage: #imageLiteral(resourceName: "Adventure"))
),
CategoryModel(
category: .artists,
categoryTitle: "By the Artist",
description: "[Insert description here]",
isPurchase: false,
categoryImage: Image(uiImage: #imageLiteral(resourceName: "Artists"))
),
]
}
SubCategoryModel:
import Foundation
import SwiftUI
//Root data -> set up data structure
//make Identifiable for ForEach looping in other views
struct SubCategoryModel: Identifiable {
let id = UUID().uuidString
let category: Category
let subcategory: String
let subtitle: String
let subdescription: String
let instruction: String
let cardImage: Image
}
class SubCategoryViewModel: ObservableObject {
#Published var gameSubCategories: [SubCategoryModel] = SubCategoryModel.allSubs
#Published var filteredCategory: [SubCategoryModel] = []
#Published var categoryType: Category = .explicit
init() {
filterByCategory()
}
func filterByCategory() {
DispatchQueue.global(qos: .userInteractive).async {
let results = self.gameSubCategories
.lazy
.filter { item in
return item.category == self.categoryType
}
DispatchQueue.main.async {
self.filteredCategory = results.compactMap({ item in
return item
})
}
}
}
}
extension SubCategoryModel {
static let allSubs = [
SubCategoryModel(
category: .explicit,
subcategory: "Significant Other",
subtitle: "Bedroom Eyes",
subdescription: "[Insert sub-description here]",
instruction: "Instructions:\n \n1) Each player pick a song\n2) Be funny, genuine, or a maverick\n3) Enjoy the trip down memory lane",
cardImage: Image(uiImage: #imageLiteral(resourceName: "Explicit"))
),
SubCategoryModel(
category: .explicit,
subcategory: "Dating",
subtitle: "First Date",
subdescription: "[Insert sub-description here]",
instruction: "Instructions:\n \n1) Each player pick a song\n2) Be funny, genuine, or a maverick\n3) Enjoy the trip down memory lane",
cardImage: Image(uiImage: #imageLiteral(resourceName: "Explicit"))
),
SubCategoryModel(
category: .adventure,
subcategory: "Hiking",
subtitle: "Bedroom Eyes",
subdescription: "[Insert sub-description here]",
instruction: "Instructions:\n \n1) Each player pick a song\n2) Be funny, genuine, or a maverick\n3) Enjoy the trip down memory lane",
cardImage: Image(uiImage: #imageLiteral(resourceName: "Adventure"))
),
]
}
My goal is to click on a card from the CategoryView screen and navigate to the SubCategoryView via a navigation link. I want the SubCategoryView to show a filtered list of subcategories based on the category selected on the CategoryView screen.
CategoryView to SubCategoryView GIF
CategoryLoop code snippet:
struct CategoryLoop: View {
let categories: [CategoryModel]
var body: some View {
ZStack {
ScrollView {
VStack {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), spacing: 20)], spacing: 20) {
ForEach(categories) { item in
NavigationLink(destination: SubCategoryView()
.navigationTitle(item.category.rawValue)) {
CategoryCard(category: item)
}
}
}
Based on the code in my CategoryLoop file, what is the best/easiest way to pass my model data and filter the list on the SubCategoryView? I am having trouble figuring out how to use the enumeration. Is it possible to write a function that would update #Published var categoryType: Category = .explicit (see SubCategoryModel) based on the card clicked in the LazyVGrid? Also, if you have suggestion on how to better organise my data models, please let me know.
Trying to go from UIKit to SwiftUI I keep wondering how to apply a service layer properly and use observables to publish updates from the service layer all the way out through the view model and to the view.
I have a View that has a View model, which derives from ObservableObject and publishes a property, displayed in the view. Using #EnvironmentObject I can easily access the view model from the view.
struct SomeView: View {
#EnvironmentObject var someViewModel: SomeViewModel
var body: some View {
TextField("Write something here", text: $someViewModel.someUpdateableProperty)
}
}
class SomeViewModel: ObservableObject {
#Published var someUpdateableProperty: String = ""
var someService: SomeService
init(someService: SomeService) {
self.someService = someService
}
// HOW DO I UPDATE self.someUpdateableProperty WHEN someService.someProperty CHANGES?
// HOW DO I UPDATE someService.someProperty WHEN self.someUpdateableProperty CHANGES?
}
class SomeService {
var someProperty: String = ""
func fetchSomething() {
// fetching
someProperty = "Something was fetched"
}
func updateSomething(someUpdateableProperty: String) {
someProperty = someUpdateableProperty
}
}
In my head a simple way could be to forward the property i.e. using computed property
class SomeViewModel: ObservableObject {
#Published var someUpdateableProperty: String {
get {
return someService.someProperty
}
set {
someService.someProperty = value
}
}
var someService: SomeService
init(someService: SomeService) {
self.someService = someService
}
}
Another approach could be if it was possible to set the two published properties equal to each other:
class SomeService: ObservableObject {
#Published var someProperty: String = ""
func fetchSomething() {
// fetching
someProperty = "Something was fetched"
}
}
class SomeViewModel: ObservableObject {
#Published var someUpdateableProperty: String = ""
var someService: SomeService
init(someService: SomeService) {
self.someService = someService
someUpdateableProperty = someService.someProperty
}
}
What is the correct way to forward a publishable property directly through a view model?
In UIKit I would use delegates or closures to call from the service back to the view model and update the published property, but is that doable with the new observables in SwiftUI?
*I know keeping the state in the service is not a good practice, but for the example let's use it.
There are, of course, lots of ways to do this, so I can't speculate on the "correct" way. But one way to do this is to remember that ObservableObject only does one thing - it has an objectWillChange publisher, and that publisher sends automatically when any of the #Published properties changes.
In SwiftUI, that is taken advantage of by the #ObservedObject, #StateObject and #EnvironmentObject property wrappers, which schedule a re-rendering of the view whenever that publisher sends.
However, ObservableObject is actually defined within Combine, and you can build your own responses to the publisher.
If you make SomeService an ObservableObject as in your last code example, then you can observe for changes in SomeViewModel:
private var cancellables = Set<AnyCancellable>()
init(someService: SomeService) {
self.someService = someService
someService.objectWillChange
.sink { [weak self] _ in
self?.objectWillChange.send()
}
.store(in: &cancellables)
}
Now, any published change to the service will cascade through to observers of the view model.
It's important to note that this publisher is will change, not did change, so if you are surfacing any properties from the service in your view model, you should make them calculated variables. SwiftUI coalesces all of the will change messages, then schedules a single view update which takes place on the next run loop, by which point the new values will be in place. If you extract values from within the sink closure, they will still represent the old data.
Is there a way to mock the viewmodels of an application that uses SwiftUI and Combine? I always find articles about mocking services used by the viewmodel but never mocking a viewmodel itself.
I tried to create protocols for each viewmodel. Problem: the #Published wrapper cannot be used in protocols. It seems like there are no solutions...
Thanks for your help
Using a protocol type as #ObservableObject or #StateObject would not work. Inheritance might be a solution (like Jake suggested), or you could go with a generic solution.
protocol ContentViewModel: ObservableObject {
var message: String { get set }
}
Your view model would be simple.
final class MyViewModel: ContentViewModel {
#Published var message: String
init(_ message: String = "MyViewModel") {
self.message = message
}
}
On the other hand, your views would be more complex using a constrained generic.
struct ContentView<Model>: View where Model: ContentViewModel {
#ObservedObject
var viewModel: Model
var body: some View {
VStack {
Text(viewModel.message)
Button("Change message") {
viewModel.message = "🥳"
}
}
}
}
The disadvantage is that you have to define the generic concrete type when using the view --- inheritance could avoid that.
// your mock implementation for testing
final class MockViewModel: ContentViewModel {
#Published var message: String = "Mock View Model"
}
let sut = ContentView<MockViewModel>(viewModel: MockViewModel())
If the view model is only a model, you should probably not mock that at all, instead you would mock the thing that modifies the view model. If your view model actually owns the functions and things that updates itself, then you would want to mock it directly. In which case you can use protocols to mock the view model. It would look a little like this.
protocol ViewModel {
var title: String { get set }
}
class MyViewModel: ViewModel {
#Published var title: String = "Page title"
}
class MockViewModel: ViewModel {
#Published var title: String = "MockPage title"
}
However, this is probably an instance where I would prefer inheriting a class instead of adhering to a protocol. Then I would override the functionality of the class for the mock.
open class ViewModel {
#Published var title: String
open fun getPageTitle() {
title = "This is the page title"
}
}
class MockViewModel: ViewModel {
override fun getPageTitle() {
title = "some other page title"
}
}
Either way would work really. The inheritance way is just less verbose in your test suite if your View Model has functionality too.
I'm using onesignal for push notifications and it was working well until the ios 14 upgrade. When I build the same app without any changes on Xcode 12, I got this warning in the console.
Notifications not accepted. You can turn them on later under your iOS
settings
There was no problem on iOS 13, it happened when I update to iOS 14.
AppDelegate.swift
import UIKit
import CoreData
import Firebase
import GoogleMobileAds
import OneSignal
import UserNotifications
import SDWebImageWebPCoder
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, OSPermissionObserver, OSSubscriptionObserver {
var window: UIWindow?
var shortcutItemToProcess: UIApplicationShortcutItem?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
GADMobileAds.sharedInstance().start(completionHandler: nil)
let onesignalInitSettings = [kOSSettingsKeyAutoPrompt: false, kOSSettingsKeyInAppLaunchURL: false]
OneSignal.initWithLaunchOptions(launchOptions,
appId: "my key is here",
handleNotificationAction: nil,
settings: onesignalInitSettings)
OneSignal.inFocusDisplayType = OSNotificationDisplayType.notification
OneSignal.promptForPushNotifications(userResponse: { accepted in
print("User accepted notifications: \(accepted)")
})
// Add your AppDelegate as an obsserver
OneSignal.add(self as OSPermissionObserver)
OneSignal.add(self as OSSubscriptionObserver)
registerForPushNotifications()
let WebPCoder = SDImageWebPCoder.shared
SDImageCodersManager.shared.addCoder(WebPCoder)
return true
}
func registerForPushNotifications() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
let readAction = UNNotificationAction(identifier: "oku", title: "Haberi Oku", options: [.foreground])
let closeAction = UNNotificationAction(identifier: "kapat", title: "Kapat", options: [])
let category = UNNotificationCategory(identifier: "etkilesim", actions: [readAction, closeAction], intentIdentifiers: [], options: [])
notificationCenter.setNotificationCategories([category])
}
func onOSPermissionChanged(_ stateChanges: OSPermissionStateChanges!) {
if stateChanges.from.status == OSNotificationPermission.notDetermined {
if stateChanges.to.status == OSNotificationPermission.authorized {
print("Thanks for accepting notifications!")
} else if stateChanges.to.status == OSNotificationPermission.denied {
print("Notifications not accepted. You can turn them on later under your iOS settings.")
}
}
}
func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges!) {
if !stateChanges.from.subscribed && stateChanges.to.subscribed {
print("Subscribed for OneSignal push notifications!")
}
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
// Grab a reference to the shortcutItem to use in the scene
if let shortcutItem = options.shortcutItem {
shortcutItemToProcess = shortcutItem
}
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .sound])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
var postId:String = ""
var postType:String = ""
if let custom = response.notification.request.content.userInfo["custom"] as? NSDictionary{
if let a = custom["a"] as? NSDictionary{
if let id = a["id"] as? String{
postId = id
}
if let type = a["rights"] as? String{
postType = type
}
}
}
if response.actionIdentifier == "oku" {
if postId != ""{
DispatchQueue.main.async(execute: {
NotificationCenter.default.post(name: NSNotification.Name("Detail"), object: nil, userInfo: ["id": postId, "type": postType])
})
completionHandler()
}
}else if response.actionIdentifier == "kapat" {
print("KAPAT")
completionHandler()
} else {
if postId != ""{
DispatchQueue.main.async(execute: {
NotificationCenter.default.post(name: NSNotification.Name("Detail"), object: nil, userInfo: ["id": postId, "type": postType])
})
completionHandler()
}
}
}
}
I solved it! :) If your app name contains non-English characters, change your Product Name under Build Settings and build it again, that's it :)
Settings
Then you can change 'Bundle display name' in info.plist.