I have a view model that looks like this:
class SegmentViewModel: ObservableObject {
#Published private(set) var itemIds = [Int]()
private var cancellables: Set<AnyCancellable> = []
init() {
fetchIds()
}
private func fetchIds() {
let request = URLRequest(path: "/ids")
network
.send(request)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error): print(error.localizedDescription)
case .finished: break
}
} receiveValue: { response in
self.itemIds = response
}
.store(in: &cancellables)
}
}
That all works fine and then my View is as follows:
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
ScrollView {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
But this doesn't work. The ProgressView is never hidden and replaced with the ScrollView. However, if I change my view to:
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
ScrollView {
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
Then it works as expected and hides the ProgressView once the itemIds aren't empty.
Why is this?
It looks like ViewBuidler consumed the condition and so it is not observed. I would wrap top at Group
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
Group { // << here !!
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
ScrollView {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
}
Related
Im learning swift and this error/warning is driving me crazy because I cant see what call Im making that causing it... The Xcode warning only shows up in my #main struct
Modifying state during view update, this will cause undefined behavior.
I thought it might be in the ListView, but I realized the warning only shows after the "Submit Post" button is it.
Im looking for a fix, but more importantly and explanation as to why this is happening and the proper usage moving forward.
import SwiftUI
import Firebase
#main
struct SocialcademyApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
PostsList()
}
}
}
struct PostsList: View {
#StateObject var viewModel = PostsViewModel()
#State private var searchText = ""
#State private var showNewPostForm = false
var body: some View {
NavigationView {
List(viewModel.posts) { post in
if searchText.isEmpty || post.contains(searchText) {
PostRow(post: post)
}
}
.searchable(text: $searchText)
.navigationTitle("Posts")
.toolbar {
Button {
showNewPostForm = true
} label: {
Label("New Post", systemImage: "square.and.pencil")
}
}
.sheet(isPresented: $showNewPostForm) {
NewPostView(creationAction: viewModel.makeCreationAction())
}
}
}
}
struct NewPostView: View {
typealias CreationAction = (Post) async throws -> Void
let creationAction: CreationAction
#State private var post = Post(title: "", content: "", authorName: "")
#State private var state = FormState.idle
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
Form {
Section {
TextField("Title", text: $post.title)
TextField("Author Name", text: $post.authorName)
}
Section {
TextField("Content", text: $post.content)
.multilineTextAlignment(.leading)
}
Button(action: createPost, label: {
if state == .working {
ProgressView() } else {
Text("Submit Post")
}
})
.font(.headline)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.padding()
.listRowBackground(Color.accentColor)
}
}
.navigationTitle("New Post")
.disabled(state == .working)
.alert("Cannot Create Post", isPresented: $state.isError, actions: {}) {
Text("Sorry, something went wrong")
}
.onSubmit {
createPost()
}
}
private func createPost() {
print("[NewPostForm] creating a new post")
Task {
state = .working
do {
try await creationAction(post)
dismiss()
} catch {
state = .error
print("[NewPostForm] Cannot create post: \(error)")
}
}
}
}
private extension NewPostView {
enum FormState {
case idle, working, error
var isError: Bool {
get {
self == .error
}
set {
guard !newValue else { return }
self = .idle
}
}
}
}
#MainActor
class PostsViewModel: ObservableObject {
#Published var posts = [Post.testPost]
func makeCreationAction() -> NewPostView.CreationAction {
return { [weak self] post in
try await PostsRepository.create(post)
self?.posts.insert(post, at: 0)
}
}
}
I am working on a social media app and I am having trouble displaying a Profile of a specific user. I am able to do it with currentUser?.uid but I don't know how to pass a different id/user to a profile view. Sorry if this explanation is confusing, I'm also having a hard time putting it into words.
This is my view that fetches the currently logged in user and displays their username:
import SwiftUI
class TestProfileViewModel: ObservableObject {
#Published var qUser: User?
init() {
fetchCurrentUser()
}
func fetchCurrentUser() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else { return }
FirebaseManager.shared.firestore.collection("users").document(uid).getDocument { snapshot, err in
if let err = err {
print("\(err)")
return
}
guard let data = snapshot?.data() else { return }
print(data)
self.qUser = .init(data: data)
}
}
}
struct TestProfileView: View {
#ObservedObject var vm = TestProfileViewModel()
var body: some View {
Text(vm.qUser?.username ?? "No User")
}
}
struct TestProfileView_Previews: PreviewProvider {
static var previews: some View {
TestProfileView()
}
}
This is the view, where I want to use the ID to fetch a user from my database and use it for a Profile View, ike how I did with the currentUser:
import SwiftUI
struct TestSongVIew: View {
let testUsername = "John"
let testUserID = "123123"
#State var showingUserProfile = false
var body: some View {
VStack {
Button("Open \(testUsername)'s Profile") {
}
}
.fullScreenCover(isPresented: $showingUserProfile, onDismiss: nil) {
TestProfileView()
}
}
}
struct TestSongVIew_Previews: PreviewProvider {
static var previews: some View {
TestSongVIew()
}
}
Here is my FirebaseManager code
import Foundation
import Firebase
import FirebaseFirestore
class FirebaseManager: NSObject {
let auth: Auth
let storage: Storage
let firestore: Firestore
static let shared = FirebaseManager()
override init() {
FirebaseApp.configure()
self.auth = Auth.auth()
self.storage = Storage.storage()
self.firestore = Firestore.firestore()
super .init()
}
}
There are a couple of ways to achieve this, and they all depend on how you set up the navigation for your app.
I'm currently working on a blog post / video to demonstrate how to monitor authentication state in a SwiftUI app. To demonstrate how to implement your use case, I added a profile screen that you can use in two ways:
You can navigate to the profile screen from the app's settings screen. This will show the user profile of the currently signed in user.
You can navigate to the profile screen from a List view showing all user profiles in your user profile collection in Firestore. This might be useful if you want to implement a high score screen that allows the user to navigate to the profile screen for each of the top 10 players in a game.
Ok, here goes:
Profile
The profile model
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct Profile: Identifiable, Codable {
#DocumentID var id: String? = ""
var nickname: String
}
extension Profile {
static let empty = Profile(nickname: "")
}
The profile view model
import Foundation
import Combine
import FirebaseFirestore
import FirebaseFirestoreSwift
class ProfileViewModel: ObservableObject {
// MARK: - Output
#Published var profile: Profile
init(profile: Profile) {
self.profile = profile
}
init(uid: String) {
self.profile = Profile.empty
fetchProfile(uid)
}
// MARK: - Private attributes
private var db = Firestore.firestore()
func fetchProfile(_ uid: String) {
db.collection("profiles")
.whereField("uid", isEqualTo: uid)
.getDocuments { querySnapshot, error in
if let error = error {
print("Error getting documents: \(error)")
}
else {
if let querySnapshot = querySnapshot {
if let document = querySnapshot.documents.first {
do {
self.profile = try document.data(as: Profile.self)
}
catch {
}
}
}
}
}
}
}
The profile view
struct ProfileView: View {
#ObservedObject var viewModel: ProfileViewModel
init(profile: Profile) {
self.viewModel = ProfileViewModel(profile: profile)
}
init(uid: String) {
self.viewModel = ProfileViewModel(uid: uid)
}
var body: some View {
Form {
Text(viewModel.profile.nickname)
}
.navigationTitle("Details")
}
}
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView(profile: Profile(nickname: "freter"))
}
}
Settings
The settings view model
import Foundation
import Combine
import FirebaseAuth
class SettingsViewModel: ObservableObject {
// MARK: - Output
#Published var email: String = ""
#Published var idToken: String = ""
#Published var user: User?
#Published var authenticationState: AuthenticationState = .unauthenticated
// MARK: - Dependencies
private var authenticationService: AuthenticationService?
func connect(authenticationService: AuthenticationService) {
if self.authenticationService == nil {
self.authenticationService = authenticationService
self.authenticationService?
.$authenticationState
.assign(to: &$authenticationState)
self.authenticationService?
.$user
.assign(to: &$user)
$user
.map { $0?.email }
.replaceNil(with: "(no email address)")
.assign(to: &$email)
}
}
#MainActor
func refreshIDToken() {
Task {
do {
idToken = try await user?.idTokenForcingRefresh(true) ?? ""
}
catch {
idToken = error.localizedDescription
print(error)
}
}
}
}
The settings view
import SwiftUI
struct SettingsView: View {
#StateObject var viewModel = SettingsViewModel()
#Environment(\.dismiss) var dismiss
#EnvironmentObject var authenticationService: AuthenticationService
#State private var presentingLoginScreen = false
var loginButton: some View {
Button(authenticationService.authenticationState == .unauthenticated ? "Login" : "Logout") {
if authenticationService.authenticationState == .unauthenticated {
presentingLoginScreen.toggle()
}
else {
authenticationService.signOut()
}
}
.frame(maxWidth: .infinity)
}
var body: some View {
Form {
Section {
Label("Help & Feedback", systemImage: "questionmark.circle")
Label("About", systemImage: "info.circle")
}
Section {
Label(viewModel.email, systemImage: "at")
Label(viewModel.idToken, systemImage: "person")
Button(action: viewModel.refreshIDToken) {
Text("Refresh ID token")
}
NavigationLink(destination: ProfileView(uid: viewModel.user?.uid ?? "unknown")) {
Label("Show user profile", systemImage: "person")
}
} header: {
Text("User Details")
}
Section {
loginButton
}
}
.sheet(isPresented: $presentingLoginScreen) {
LoginView()
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
}
.onAppear {
viewModel.connect(authenticationService: authenticationService)
}
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
SettingsView()
.environmentObject(AuthenticationService())
}
}
}
Authentication
The authentication service
import Foundation
import FirebaseAuth
enum AuthenticationState {
case unauthenticated
case authenticating
case authenticated
}
class AuthenticationService: ObservableObject {
// MARK: - Output
#Published var authenticationState: AuthenticationState = .unauthenticated
#Published var errorMessage: String = ""
#Published var user: User?
init() {
registerAuthStateListener()
}
#MainActor
func signIn(withEmail email: String, password: String) async -> Bool {
authenticationState = .authenticating
do {
try await Auth.auth().signIn(withEmail: email, password: password)
return true
}
catch {
await MainActor.run {
errorMessage = error.localizedDescription
authenticationState = .unauthenticated
}
print(error)
return false
}
}
func signOut() {
do {
try Auth.auth().signOut()
}
catch {
print(error)
}
}
private var handle: AuthStateDidChangeListenerHandle?
private func registerAuthStateListener() {
if handle == nil {
handle = Auth.auth().addStateDidChangeListener { auth, user in
Task {
await MainActor.run {
self.user = user
if let user = user {
self.authenticationState = .authenticated
print("User \(user.uid) signed in. Email: \(user.email ?? "(no email address set)"), anonymous: \(user.isAnonymous)")
}
else {
self.authenticationState = .unauthenticated
print("User signed out.")
}
}
}
}
}
}
}
The login view model
import Foundation
import Combine
import FirebaseAuth
class LoginViewModel: ObservableObject {
// MARK: - Input
#Published var email: String = ""
#Published var password: String = ""
// MARK: - Output
#Published var isValid: Bool = false
#Published var authenticationState: AuthenticationState = .unauthenticated
#Published var errorMessage: String = ""
#Published var user: User?
// MARK: - Dependencies
private var authenticationService: AuthenticationService?
func connect(authenticationService: AuthenticationService) {
if self.authenticationService == nil {
self.authenticationService = authenticationService
self.authenticationService?
.$authenticationState
.assign(to: &$authenticationState)
self.authenticationService?
.$errorMessage
.assign(to: &$errorMessage)
self.authenticationService?
.$user
.assign(to: &$user)
Publishers.CombineLatest($email, $password)
.map { !($0.isEmpty && $1.isEmpty) }
.print()
.assign(to: &$isValid)
}
}
func signInWithEmailPassword() async -> Bool {
if let authenticationService = authenticationService {
return await authenticationService.signIn(withEmail: email, password: password)
}
else {
return false
}
}
}
The login view
import SwiftUI
enum FocusableField: Hashable {
case email
case password
}
struct LoginView: View {
#StateObject var viewModel = LoginViewModel()
#EnvironmentObject var authenticationService: AuthenticationService
#Environment(\.dismiss) var dismiss
#FocusState private var focus: FocusableField?
private func signInWithEmailPassword() {
Task {
if await viewModel.signInWithEmailPassword() == true {
dismiss()
}
}
}
var body: some View {
VStack {
Image("Login")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minHeight: 0)
Text("Login")
.font(.largeTitle)
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .leading)
HStack {
Image(systemName: "at")
TextField("Email", text: $viewModel.email)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.focused($focus, equals: .email)
.submitLabel(.next)
.onSubmit {
self.focus = .password
}
}
.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 4)
HStack {
Image(systemName: "lock")
SecureField("Password", text: $viewModel.password)
.focused($focus, equals: .password)
.submitLabel(.go)
.onSubmit {
signInWithEmailPassword()
}
}
.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 8)
if !viewModel.errorMessage.isEmpty {
VStack {
Text(viewModel.errorMessage)
.foregroundColor(Color(UIColor.systemRed))
}
}
Button(action: signInWithEmailPassword) {
if viewModel.authenticationState != .authenticating {
Text("Login")
.frame(maxWidth: .infinity)
}
else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.frame(maxWidth: .infinity)
}
}
.disabled(!viewModel.isValid)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.controlSize(.large)
HStack {
VStack { Divider() }
Text("or")
VStack { Divider() }
}
Button(action: { }) {
Image(systemName: "applelogo")
.frame(maxWidth: .infinity)
}
.foregroundColor(.black)
.buttonStyle(.bordered)
.controlSize(.large)
HStack {
Text("Don't have an account yet?")
Button(action: {}) {
Text("Sign up")
.fontWeight(.semibold)
.foregroundColor(.blue)
}
}
.padding([.top, .bottom], 50)
}
.onAppear {
viewModel.connect(authenticationService: authenticationService)
}
.listStyle(.plain)
.padding()
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
Group {
LoginView()
.environmentObject(AuthenticationService())
LoginView()
.preferredColorScheme(.dark)
.environmentObject(AuthenticationService())
}
}
}
ViewModel
Api is called here and response is stored in watchLaterDataArr the variable
class HomeViewModel: ObservableObject {
#Published var pageCount = ""
#Published var hasNoMoreRows = false
#Published var watchLaterDataArr = [WatchLaterModelResponseData]()
init(apiService: IStudioRepository) {
self.apiService = apiService
}
func getWatchLaterListAPI() {
apiService.getWatchLaterShoSeriesPage(page: self.pageCount)
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [weak self] (completion) in
switch completion {
case .finished:
break
case .failure(let error):
}
}, receiveValue: { response in
self.getStudioLatestReleasesSuccess(response)
})
.store(in: &self.cancellableBag)
}
func getStudioLatestReleasesSuccess(_ result: WatchLaterModel) {
if(result.statusCode == 200) {
for i in 0..<(result.response?.data.count)!{
if !self.hasNoMoreRows {
self.watchLaterDataArr.append((result.response?.data[i])!)
}
}
if result.response?.nextPage != nil && (result.response?.nextPage ?? "") != ""{
self.pageCount = result.response?.nextPage ?? ""
getWatchLaterListAPI()
}
else {
print(self.watchLaterDataArr.count)
self.hasNoMoreRows = true
}
}
}
}
Views In Views the data is not reflected the if condition is not satisfied or not called even when the view model is observableobject and publisher variable is used.
struct WatchLaterView: View {
#Environment(\.presentationMode) var presentation
#EnvironmentObject var watchLaterViewModel:HomeViewModel
var body: some View {
VStack {
VStack{
if (self.watchLaterViewModel.watchLaterDataArr.count > 0 && self.watchLaterViewModel.hasNoMoreRows) {
QGrid(self.watchLaterViewModel.watchLaterDataArr, columns: 3) {
StudioWatchLaterCellView(imageUrl: $0.thumbnailImage ?? "",videoItem: $0)
}
}
Spacer()
}
}.onAppear {
self.watchLaterViewModel.getWatchLaterListAPI()
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
HomeView is called is tabBar like
HomeView().environmentObject(HomeViewModel(apiService: Constants.studioApiService))
.tabItem {
Text("Home")
self.isSelected == 0 ? Image("Home_selected") : Image("Home")
} .tag(0)
Help please the code is too big i can't shorten it anymore.
I have a database with several objects with booleans as attribute. I'm looking for a function to invert all boolean objects when I press a button. I tried this function but several errors are displayed like (Value of type 'Bool' has no member 'indices') :
struct ViewList: View {
#Environment(\.managedObjectContext) var context
#State var newName: String = ""
#FetchRequest(
entity: Product.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Product.name, ascending: true)]
) var list: FetchedResults<Product>
var body: some View {
VStack {
HStack {
TextField("I insert the name of the product", text: $newName)
Button(action: { self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
List {
ForEach(list, id: \.self) {
product in ViewItem(product: product)
}
}
}
}
public func add() {
let newProduct = Product(context: context)
newProduct.name = newName
do {
try context.save()
} catch {
print(error)
}
}
}
struct ViewItem: View {
#State var product: Product
#State var refresh: Bool = false
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack(alignment: .top) {
Button( action: {
self.clean()
self.product.isSelected.toggle()
}) {
if self.product.isSelected == true {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
VStack() {
Text(product.name)
if product.password != "" {
Text("Password : " + product.password)
}
Text(String(refresh)).hidden()
}
}
}
.onAppear {
self.refresh = false
}
}
}
I've been thinking about it, but I don't know how to go about it...
func clean() {
for( index ) in self.product.isSelected.indices {
self.product[index]isSelected = false
}
}
You need to create a query to flip the state of the isSelected flag. This logic is best kept out of the view system so you can use it anywhere.
You create a SelectionHandler
import CoreData
class SelectionHandler {
func clearSelection(in context: NSManagedObjectContext) {
for item in currentSelected(in: context) {
item.isSelected = false
}
}
func selectProduct(_ product: Product) {
guard let context = product.managedObjectContext else {
assertionFailure("broken !")
return
}
clearSelection(in: context)
product.isSelected = true
}
func currentSelected(in context: NSManagedObjectContext) -> [Product] {
let request = NSFetchRequest<Product>(entityName: Product.entity().name!)
let predicate = NSPredicate(format: "isSelected == YES")
request.predicate = predicate
do {
let result = try context.fetch(request)
return result
} catch {
print("fetch error =",error)
return []
}
}
}
which you can then use to select your desired product.
SelectionHandler().selectProduct(product)
As it stands your NavigationLink will do nothing because the parent list is not held in a NavigationView so you'll need to change the body of ViewList to look like this.
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Create product with name", text: $newName)
Button(action: {
self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
.padding()
List {
ForEach(list, id: \.self) { product in
ViewItem(product: product)
}
}
}
}
}
and in ViewItem , Product should be an ObservedObject so that changes are detected in the managedObject.
struct ViewItem: View {
#ObservedObject var product: Product
#State var refresh: Bool = false
var checkmarkImage: some View {
return Group {
if self.product.isSelected {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
}
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack {
checkmarkImage
Text(product.name ?? "wat")
}
}
}
}
The original Button won't play with the NavigationLink but you can simply apply the selection to onAppear in ViewDetail
struct ViewDetail: View {
#ObservedObject var product: Product
#Binding var refresh: Bool
var body: some View {
VStack {
Text("Hello, World!")
Text("Product is \(product.name ?? "wat")")
}
.onAppear {
SelectionHandler().selectProduct(self.product)
}
}
}
I'm trying to make a simple Master-Detail-FileViewer app. In the last FileViewer view I want to have a button, which has an option to make the file favourite (every file has an "id" string, which is appended to an Environment object). When you favour them, this object is shown at the master view for quick access for the user, linking to the FileViewer view. However, when the user taps and goes there, the button is inactive - you cannot tap it and it gets black from blue. If you want to remove them from favourites, you can't.
I'd really appreciate to tell me what is wrong and how to make the button active. No error is shown and the app doesn't crash. It just doesn't work.
Thanks in advance!
The files are either "judgement" and "secondary", both have id and title properties. The second picture is the problematic one.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
NavigationView {
List {
NavigationLink(destination: JudgementsView()) {
Text("Judgements")
}
NavigationLink(destination: SecondaryView()) {
Text("Secondary acts")
}
ScrollView(.horizontal, showsIndicators: false) {
VStack {
if favouriteList.items.isEmpty {
Text("Nothing favoured")
} else {
ForEach(favouriteList.items, id: \.self) { id in
VStack {
HStack {
ForEach(judgementsTAXraw.filter {
$0.id == id
}) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title).padding()
}
}
}
HStack {
ForEach(secondaryTAXraw.filter {
$0.id == id
}) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title).padding()
}
}
}
}
}
}
}
}
}
.navigationBarTitle(Text("Test"))
}
}
}
struct JudgementsView: View {
var body: some View {
List(judgementsTAXraw, id: \.id) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title)
}
}
}
}
struct SecondaryView: View {
var body: some View {
List(secondaryTAXraw, id: \.id) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title)
}
}
}
}
struct FileViewer: View {
var file: String
#State private var showCopySheet = false
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
Text(file)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.showCopySheet = true
}) {
Image(systemName: "doc.on.doc").frame(minWidth: 40)
}.actionSheet(isPresented: $showCopySheet) {
ActionSheet(title: Text("What do you want to do?"), buttons: [
.destructive(Text("favText"), action: {
if let index = self.favouriteList.items.firstIndex(of: self.file) {
self.favouriteList.items.remove(at: index)
} else {
self.favouriteList.items.append(self.file)
}
}),
.cancel()
])
}
)
}
}
Aaaand in a separate file is the object:
import Foundation
class FavouritesList: ObservableObject {
#Published var items = [String]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "FavouredItems")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "FavouredItems") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([String].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}