I am passing a view model to a view using environment objects. For the sake of previewing different states how can I change some properties in the environment object before passing it in the preview?
I'd like to do something like this but this doesnt work:
struct view_Previews: PreviewProvider {
#EnvironmentObject static var authenticationViewModel: AuthenticationViewModel {
get {
let v = authenticationViewModel
v.showResendCodeTimer = true
return v
}
}
static var previews: some View {
SomeView().environmentObject(authenticationViewModel)
}
}
Here is possible approach
static var previews: some View {
Group {
SomeView().environmentObject({ () -> AuthenticationViewModel in
let v = AuthenticationViewModel()
v.showResendCodeTimer = true
return v
}())
SomeView().environmentObject({ () -> AuthenticationViewModel in
let v = AuthenticationViewModel()
v.showResendCodeTimer = false
return v
}())
}
}
Related
this is my PersistenceController:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentCloudKitContainer(name: "LogModel")
container.loadPersistentStores {(descr, error) in
if let error = error as NSError? {
fatalError("Container load failed: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}
here I inject the container.
import SwiftUI
#main
struct MyApp: App {
var persistanceContainer = PersistenceController()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(CoreBluetoothViewModel())
.environment(\.managedObjectContext, persistanceContainer.container.viewContext)
}
}
}
I want to use this View to toggle between Cloud and Local store.
struct iCloudSyncView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var cloudEnabled = true
var body: some View {
Toggle("iCloud sync", isOn: $cloudEnabled)
}
}
I know that i have to use the NSPersistentCloudKitContainerfor the cloud and the NSPersistentCloudKitContainerfor the local store.
Also I found this article:
CoreData+CloudKit | On/off iCloud sync toggle
But I just can't get it to work :/
Can someone please tell me how I have to implement this?
I would add it like this:
struct PersistenceController {
let iCloud = UserDefaults.standard.bool(forKey: "iCloud")
static let shared = PersistenceController()
var container: NSPersistentContainer
init() {
if iCloud {
container = NSPersistentCloudKitContainer(name: "LogModel")
} else {
container = NSPersistentContainer(name: "LogModel")
}
container.loadPersistentStores {(descr, error) in
if let error = error as NSError? {
fatalError("Container load failed: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}
But I don't know how to load the new container when toggle is changed.
Toggle("iCloud sync", isOn: $cloudEnabled)
.onChange(of: cloudEnabled) { value in
saveContext()
UserDefaults.standard.set(value, forKey: "iCloud")
//get new container
}
There are 2 ways
add local and cloud configurations to the model editor and set them on the store description and assign different entities to each config.
Use affectedStores for fetching and assign(to:) when creating new objects to manually select the store.
1 is best.
I use Realm + SwiftUI 2.0, iOS 14. I successfully linked (read/write) Realm to Swift 2.0 iOS app.
final class DataEntryStore: ObservableObject {
private var entryCancellable: AnyCancellable?
private(set) var entryDB = DataObservable<Entry>()
// could store related references to other related DataObservables
#Published private(set) var entries: [Entry] = []
// MARK: - init
init() {
entryDB = DataObservable<Entry>()
entryCancellable = entryDB.$items.assign(to: \.entries, on: self)
}
}
class RealmEntry: Object, UUIDIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var entryType = ""
#objc dynamic var entryDate = Date()
#objc dynamic var note: String?
#objc dynamic var trainingType: String?
#objc dynamic var trainingTime = 0
override static func primaryKey() -> String? {
return "id"
}
}
// MARK: - Abstracted Data Struct which is what is presented with the UI
// UI Model
struct Entry: Hashable, RealmConvertible {
typealias RealmType = RealmEntry
// MARK: - Properties
var id: String
var entryType: DataEntryType
var entryDate: Date
var note: String?
var trainingType: TrainingType?
var trainingTime: Int
func toRealmObject() -> RealmEntry {
let realmObj = RealmEntry()
realmObj.id = id
realmObj.entryType = entryType.rawValue
realmObj.entryDate = entryDate
realmObj.note = note
if let trainingTypeValue = trainingType?.rawValue {
realmObj.trainingType = trainingTypeValue
}
realmObj.trainingTime = trainingTime
return realmObj
}
static func fromRealmObject(_ obj: RealmEntry) -> Entry {
Entry(obj)
}
init() {
self.id = UUID().uuidString
self.entryType = .note
self.entryDate = Date()
self.trainingTime = 0
}
init(_ obj: RealmEntry) {
self.id = obj.id
self.entryType = DataEntryType(fromRawValue: obj.entryType)
self.entryDate = obj.entryDate
self.note = obj.note
if let trainingType = obj.trainingType {
self.trainingType = TrainingType(fromRawValue: trainingType)
}
self.trainingTime = obj.trainingTime
}
}
protocol StringIdentifiable {
var id: String { get }
}
protocol UUIDIdentifiable: Identifiable { var id: String { get } }
//protocol Initializable { init() }
// MARK: - Map Between the Two
protocol RealmConvertible where Self: Equatable & UUIDIdentifiable {
associatedtype RealmType: Object & UUIDIdentifiable
func toRealmObject() -> RealmType
init(_ dest: RealmType)
}
// Dynamic Realm Binding for live data editing
extension RealmConvertible {
func realmBinding() -> Binding<Self> {
let h = RealmHelper()
return Binding<Self>(get: {
if let r = h.get(self.toRealmObject()) {
// get the latest realm version for most uptodate data and map back to abstracted structs on init
return Self(r)
} else {
// otherwise return self as it's the most uptodate version of the data struct
return self
}
}, set: h.updateConvertible)
}
}
However, I am having an issue when tried to link (read) it to Widget Extension.
#main
struct MyWidget: Widget {
let kind = "MyWidget"
var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetView()
}
.supportedFamilies([.systemSmall, .systemMedium])
.configurationDisplayName("My Widget")
.description("This is widget.")
}
}
struct WidgetView: View {
#EnvironmentObject var data: DataEntryStore
var body: some View {
Text(data.getRealmData)
ViewWithRealmDataFromMainApp()
}
}
I have tried to follow these instructions by using "App Groups" and Shared Objects to Main iOS App and to Widget Extension, but it didn't work.
More than that, if I link it to Realm, I can't even show static data on the widget. It shows blurry rectangles.
How to show data from the Realm on the iOS 14 widget?
https://i.stack.imgur.com/yzUG1.jpg
https://i.stack.imgur.com/nKy6i.jpg
I have a view that listens to a Model via and ObservableObject:
class Feed : ObservableObject {
// Posts to be displayed
#Published var posts = [Posts]()
...
...
}
And the Posts model looks like:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
}
Which contains multiple other Struct models like Friends and Bars. However, when I do change a value within one of these other models, it doesn't trigger the #Published to fire, so the view isn't redrawn. For example, the Friends model looks like:
struct Friends : Hashable {
static func == (lhs: Friends, rhs: Friends) -> Bool {
return lhs.id == rhs.id
}
let name: String
let username: String
let id : String
var thumbnail : UIImage?
var profileImgURL : String?
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
but when I change the thumbnail, the views are not redrawn. But when I change something directly apart of the Posts model, like the description attribute, the view is redrawn. How am I able to have the view redraw when the underlying model values are changed?
I change the thumbnail as shown:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
}
}
}
}
But if I were to change the description doing the same thing:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
And when I do this, the view does update and change. I can see that the thumbnails are being loaded correctly, too, because I can print out the data sent, and sometimes the thumbnails are redrawn for the view correctly.
EDIT
As suggested I tried adding a mutating func to the struct:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
mutating func addInitThumbnail(img : UIImage) {
self.intializer.thumbnail = img
}
}
and then using it:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
if let thumbnailImg = UIImage(data: data) {
self.posts[indexOfPost].addInitThumbnail(img: thumbnailImg)
}
}
}
}
}
}
but it did not work either.
However, when I do:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
}
the images are loaded and set correctly...? So I think it might have something to do with UIImages directly?
I tried using mutating function and also updating value directly, both cases it worked.
UPDATED CODE (Added UIImage in new struct)
import SwiftUI
import Foundation
//Employee
struct Employee : Identifiable{
var id: String = ""
var name: String = ""
var address: Address
var userImage: UserIcon
init(name: String, id: String, address: Address, userImage: UserIcon) {
self.id = id
self.name = name
self.address = address
self.userImage = userImage
}
mutating func updateAddress(with value: Address){
address = value
}
}
//User profile image
struct UserIcon {
var profile: UIImage?
init(profile: UIImage) {
self.profile = profile
}
mutating func updateProfile(image: UIImage) {
self.profile = image
}
}
//Address
struct Address {
var houseName: String = ""
var houseNumber: String = ""
var place: String = ""
init(houseName: String, houseNumber: String, place: String) {
self.houseName = houseName
self.houseNumber = houseNumber
self.place = place
}
func getCompleteAddress() -> String{
let addressArray = [self.houseName, self.houseNumber, self.place]
return addressArray.joined(separator: ",")
}
}
//EmployeeViewModel
class EmployeeViewModel: ObservableObject {
#Published var users : [Employee] = []
func initialize() {
self.users = [Employee(name: "ABC", id: "100", address: Address(houseName: "Beautiful Villa1", houseNumber: "17ABC", place: "USA"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "XYZ", id: "101", address: Address(houseName: "Beautiful Villa2", houseNumber: "18ABC", place: "UAE"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "QWE", id: "102", address: Address(houseName: "Beautiful Villa3", houseNumber: "19ABC", place: "UK"), userImage: UserIcon(profile: UIImage(named: "discover")!))]
}
func update() { //both below cases worked
self.users[0].address.houseName = "My Villa"
//self.users[0].updateAddress(with: Address(houseName: "My Villa", houseNumber: "123", place: "London"))
self.updateImage()
}
func updateImage() {
self.users[0].userImage.updateProfile(image: UIImage(named: "home")!)
}
}
//EmployeeView
struct EmployeeView: View {
#ObservedObject var vm = EmployeeViewModel()
var body: some View {
NavigationView {
List {
ForEach(self.vm.users) { user in
VStack {
Image(uiImage: user.userImage.profile!)
Text("\(user.name) - \(user.address.getCompleteAddress())")
}
}.listRowBackground(Color.white)
}.onAppear(perform: fetch)
.navigationBarItems(trailing:
Button("Update") {
self.vm.update()
}.foregroundColor(Color.blue)
)
.navigationBarTitle("Users", displayMode: .inline)
}.accentColor(Color.init("blackTextColor"))
}
func fetch() {
self.vm.initialize()
}
}
it's been a long time but still :
1 - mutating func is not necessary.
2 - The re-rendering won't happen if you only change the nested object and not the "observed" object it self.
3 - You can play with the getters and setters as well, to pull the wanted value to change and update it back.
Considering we have a complex object such as :
struct Content{
var listOfStuff : [Any] = ["List", 2, "Of", "Stuff"]
var isTheSkyGrey : Bool = false
var doYouLikeMyMom : Bool = false
var status : UIImage? = UIImage(systemName: "paperplane")
}
Now let's wrap/nest this object into a ContentModel for the View. If, while using the #State var contentModel : ContentModel in the View, we change change one of the properties directly by accessing the nested object(like so : model.content.status = "Tchak"), it will not trigger a re-rendering because the ContentModel itself didn't change.
Understanding this, we need to trigger a tiny useless change in the ContentModel :
struct ContentModel {
private var change : Bool = false
private var content : Content = Content() {
didSet{
// this will trigger the view to re-render
change.toggle()
}
}
//the value you want to change
var status : UIImage?{
get{
contentModel.status
}
set{
contentModel.status = newValue
}
}
}
Now what's left to do is to observe the change of the content in the view.
struct ContentPouf: View {
#State var contentModel = ContentModel()
var body: some View {
Image(uiImage: contentModel.status)
.onTapGesture {
contentModel.status = UIImage(systemName: "pencil")
}
}
}
and using an ObservableObject it would be :
class ContentObservable : ObservableObject {
#Published var content : ContentModel = ContentModel()
func handleTap(){
content.status = UIImage(systemName: "pencil")
}
}
and
#StateObject var viewModel : ContentObservable = ContentObservable()
var body: some View {
Image(uiImage :viewModel.content.status)
.onTapGesture {
viewModel.handleTap()
}
}
I have the following classes
class ListItem: Identifiable {
var id: UUID
var name: String
var description: String
var isFavorite: Bool
var debugDescription: String {
return "Name: \(self.name) | Favorite?: \(self.isFavorite)"
}
public init(name: String) {
self.name = name
id = UUID()
self.description = "Some text describing why \(self.name.lowercased()) is awesome"
self.isFavorite = false
}
}
class ListItems: ObservableObject {
#Published var items: [ListItem]
let defaultAnimals = ["Ant", "Bear", "Cat", "Dog", "Elephant",
"Fish", "Giraffe", "Hyena", "Iguana", "Jackal", "Kingfisher", "Leopard", "Monkey"]
public init(animals: [String] = []) {
let animalList: [String] = animals.count > 0 ? animals : defaultAnimals
self.items = animalList.sorted {
$0.lowercased() < $1.lowercased()
}.map {
ListItem(name: $0.firstUppercased)
}
}
}
and the following image view in ContentView
struct ContentView: View {
#ObservedObject var list: ListItems = ListItems()
var body: some View {
List(list.items) {
animal in HStack {
// ...
Image(systemName: animal.isFavorite ? "heart.fill" : "heart").foregroundColor(.pink).onTapGesture {
let index = self.list.items.firstIndex { $0.id == animal.id } ?? -1
if (index >= 0) {
self.list.items[index].isFavorite = !animal.isFavorite
self.list.items = Array(self.list.items[0...self.list.items.count-1]) // <--
}
}
// ...
}
}
}
}
Everytime, the image view is tapped, I am basically reassigning the entire array like this so that the changes can be reflected in the UI
self.list.items = Array(self.list.items[0...self.list.items.count-1])
My question: How can I refactor my code to prevent reassigning the entire object array every time some object property changes?
I am fairly new to Swift & iOS development, not sure if I am missing something basic.
Declare ListItem as an struct instead of a class, this way the view will be notified when isFavorite changes. And just a little suggestion; you can use toggle to change the value of a boolean: self.list.items[index].isFavorite.toggle()
I'm curious about the default implementation of AnyView in SwiftUI. How to put structs with different generic types into a protocol array?
For example:
let a = AnyView(Text("hello"))
let b = AnyView(Image(systemName: "1.circle"))
let genericViews = [a, b] // No compile error
And my implementation:
struct TypeErasedView<V: View>: View {
private var _view: V
init(_ view: V) {
_view = view
}
var body: V {
_view
}
}
let a = TypeErasedView(Text("Hello"))
let b = TypeErasedView(Image(systemName: "1.circle"))
let genericViews = [a, b] // compile error
The compile error will be "Heterogeneous collection literal could only be inferred to '[Any]'; add explicit type annotation if this is intentional".
Does anyone have any ideas?
Here is a demo of possible approach. It is simplified, but shows the generic idea of how this might be done... or at least a direction.
Full compilable & working module. Tested on Xcode 11.2 / iOS 13.2
import SwiftUI
private protocol TypeErasing {
var view: Any { get }
}
private struct TypeEraser<V: View>: TypeErasing {
let orinal: V
var view: Any {
return self.orinal
}
}
public struct MyAnyView : View {
public var body: Never {
get {
fatalError("Unsupported - don't call this")
}
}
private var eraser: TypeErasing
public init<V>(_ view: V) where V : View {
eraser = TypeEraser(orinal: view)
}
fileprivate var wrappedView: Any { // << they might have here something specific
eraser.view
}
public typealias Body = Never
}
struct DemoAnyView: View {
let container: [MyAnyView]
init() {
let a = MyAnyView(Text("Hello"))
let b = MyAnyView(Image(systemName: "1.circle"))
container = [a, b]
}
var body: some View {
VStack {
// dynamically restoring types is different question and might be
// dependent on Apple's internal implementation, but here is
// just a demo that it works
container[0].wrappedView as! Text
container[1].wrappedView as! Image
}
}
}
struct DemoAnyView_Previews: PreviewProvider {
static var previews: some View {
DemoAnyView()
}
}
It's because there's a generic constraint on yours. AnyView has no generic constraint. You instantiate it with an underlying generic View, but its Body is always declared as Never. There might be compiler magic happening here as I couldn't get a generic constraint-less version to work.