Unable to access variable values in view - ios

Trying to send email from my iOS app. It have it set up and it's good to go, but I can't seem to be able to get the text passed to the view presented when sending the email. When I pass the text to be sent, it's always empty.
I know it might be related to the view not having access to it, but I'm scratching my head what to change, or what to add in order to make it work. I have tried with #binding and ObservableObject, but I'm still new with Swift and SwiftUI, so I'm making a mess.
Here's the code, how can I pass the text from the list item to the new view presented?
struct ContentView: View {
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
// added this to try to force the text to go, since passing jot.text was giving me always
// the first item in the list
#State private var emailText: String = ""
var body: some View {
NavigationView {
List(jots) { jot in
Text(jot.text!)
.contextMenu {
if MFMailComposeViewController.canSendMail() {
Button(action: {
emailText = jot.text! // try to force the text to be passed
self.isShowingMailView.toggle()
}) {
Text("Email jot")
Image(systemName: "envelope")
}
}
}
.sheet(isPresented: $isShowingMailView) {
MailView(result: $result) { composer in
composer.setSubject("Jot!")
// in here, if I pass jot.text! then it's always the first item in the list
// if I pass emailText then it's always empty
composer.setMessageBody(emailText, isHTML: false)
}
}
}
.listStyle(.plain)
}
}
}
And the supporting code to send email:
import SwiftUI
import UIKit
import MessageUI
public struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
public var configure: ((MFMailComposeViewController) -> Void)?
public class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
public func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
public func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
configure?(vc)
return vc
}
public func updateUIViewController(
_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}

We don't have a full Minimal Reproducible Example (MRE), but I think what you want is to use the sheet(item:onDismiss:content:) initializer. Instead of using a Bool to trigger the sheet showing, it triggers when an optional value of whatever data you wish to pass in becomes non-nil. This way, you can pass the data to the .sheet and only need one variable to do it. This is untested, but try:
struct ContentView: View {
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
// this is your optional selection variable
#State private var selectedJot: Jot?
var body: some View {
NavigationView {
List(jots) { jot in
Text(jot.text!)
.contextMenu {
if MFMailComposeViewController.canSendMail() {
Button(action: {
// this gives selectedJot a value making it non-nil
selectedJot = jot
}) {
Text("Email jot")
Image(systemName: "envelope")
}
}
}
}
.listStyle(.plain)
// When selectedJot becomes non-nil, this initializer will trigger the sheet.
.sheet(item: $selectedJot) { jot in
MailView(result: $result) { composer in
composer.setSubject("Jot!")
composer.setMessageBody(jot.text, isHTML: false)
}
}
}
}
}

Related

In SwiftUI how do I update a Published property inside ViewModel1 from ViewModel2?

Fairly new to SwiftUI and trying to figure out how to use ViewModels. Coming from UIKit I tend to like binding button presses to view model events, then apply the business logic and return a new value.
I am trying this in SwiftUI:
struct MainView: View {
#ObservedObject private var viewModel: MainViewModel
#State private var isShowingBottomSheet = false
var body: some View {
VStack {
Text("Hello \(viewModel.username)")
.font(.title)
Button("Show bottom sheet") {
isShowingBottomSheet = true
}
.sheet(isPresented: $isShowingBottomSheet) {
let viewModel = SheetViewModel()
viewModel.event.usernameUpdated
.assign(to: &$viewModel.username)
SheetView(viewModel: viewModel)
.presentationDetents([.fraction(0.15), .medium])
}
}
}
// MARK: - Initializers
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
}
With the view model:
final class MainViewModel: ObservableObject {
// MARK: - Properties
#Published var username = "John"
}
And SheetView:
struct SheetView: View {
#ObservedObject private var viewModel: SheetViewModel
var body: some View {
VStack {
Text("Some Sheet")
.font(.title)
Button("Change Name") {
viewModel.event.updateUsernameButtonTapped.send(())
}
}
}
// MARK: - Initializers
init(viewModel: SheetViewModel) {
self.viewModel = viewModel
}
}
And SheetViewModel:
final class SheetViewModel: ObservableObject {
// MARK: - Events
struct Event {
let updateUsernameButtonTapped = PassthroughSubject<Void, Never>()
let usernameUpdated = PassthroughSubject<String, Never>()
}
// MARK: - Properties
let event = Event()
private var cancellables = Set<AnyCancellable>()
// MARK: - Binding
private func bindEvents() {
event.updateUsernameButtonTapped
.map { "Sam" }
.sink { [weak self] name in
self?.event.usernameUpdated.send(name)
}
.store(in: &cancellables)
}
}
I am getting the error Cannot convert value of type 'Binding<String>' to expected argument type 'Published<String>.Publisher'. I want my SheetViewModel to update the value of #Published var username in the MainViewModel. How would I go about this?
We usually don't need view model objects in SwiftUI which has a design that benefits from value semantics, rather than the more error prone reference semantics of UIKit. If you want to move logic out of the View struct you can group related state vars and mutating funcs in their own struct, e.g.
struct ContentView: View {
#State var config = SheetConfig()
var body: some View {
VStack {
Text(config.text)
Button(action: show) {
Text("Edit Text")
}
}
.sheet(isPresented: $config.isShowing,
onDismiss: didDismiss) {
TextField("Text", $config.text)
}
}
func show() {
config.show(initialText: "Hello")
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct SheetConfig {
var text = ""
var isShowing = false
mutating func show(initialText: String) {
text = initialText
isShowing = true
}
}
If you want to persist/sync data, or use Combine then you will need to resort to the reference type version of state which is #StateObject. However if you use the new async/await and .task then it's possible to still not need it.

Failing to send email

I have a button that allows you to send an email with all the content on your app.
I'm iterating thru all the data stored in a core data container, and creating a string that I then pass to the sheet presenting the ability to send email.
When I test it, the string always seems to be empty, and I can see an error: [PPT] Error creating the CFMessagePort needed to communicate with PPT.
I'm using the same mechanism I use to email each item on the list, which works like a charm.
Anyway, I've see a lot of posts about the error, but nothing that points to a solution.
Here's the code, maybe it's related to how I call the .sheet? What am I missing? What's that error even try to tell me?
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State private var sheetbackupJotMail = false
//for sending mail
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
#State private var emailText: String = ""
var body: some View {
NavigationView {
List (jots) { jot in
Text(jot.text!)
}
}
.toolbar {
// toolbar button that send the message with all content
ToolbarItem(placement: .navigation) {
if MFMailComposeViewController.canSendMail() {
Button(action: {
sheetbackupJotMail.toggle()
}) {
Label("Back up all jots", systemImage: "arrow.up.square").foregroundColor(.secondary)
}
// sheet for backing up email
.sheet(isPresented: $sheetbackupJotMail) {
MailView(result: $result) { composer in
emailText = ""
for jot in jots {
emailText = emailText + jot.dateText! + "\n" + jot.text! + "\n\n"
}
print(">>>: " + emailText) //<-- this is always empty, however if I move to the button before the toggle(), I get the right text
// emailing all
composer.setSubject("Jot Backup")
composer.setMessageBody(emailText, isHTML: false)
}
}
}
}
}
}
}
// mail view
import SwiftUI
import UIKit
import MessageUI
public struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
public var configure: ((MFMailComposeViewController) -> Void)?
public class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
public func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
public func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
configure?(vc)
return vc
}
public func updateUIViewController(
_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
Also moving the creation of the text here caused the text I need to mail to be ok, but the error continues to be: PPT] Error creating the CFMessagePort needed to communicate with PPT.
Button(action: {
emailText = ""
for jot in jots {
emailText = emailText + jot.dateText! + "\n" + jot.text! + "\n\n"
}
print(">>>: " + emailText)
sheetbackupJotMail.toggle()
}) {
Label("Back up all jots", systemImage: "arrow.up.square").foregroundColor(.secondary)
}
The sheet content is computed only once on creation time, so all later changes in dependencies are not re-injected, so you have to put everything dependent by binding inside MailView and do composing there.
I.e. your sheet should look like (sketch)
.sheet(isPresented: $sheetbackupJotMail) {
MailView(result: $result, emailText: $emailText, jots: $jots)
}
*Many dependencies are missing, so it is possible to provide testable solution, but the idea should be clear. See also https://stackoverflow.com/a/64554083/12299030 - it demos solution for similar issue.

SwiftUI: Saving likes in CoreData for each individual cell

How do I save the like state for each individual cell? I decided to save via CoreData, but the like is saved for all cells at once.
In the Core Data model (LikedDB) there is an attribute such as isLiked
In the class there is a variable isLiked, which changes the state of the like:
class ModelsViewModel: ObservableObject{
#Published var isLiked = false
func like() {
isLiked.toggle()
}
}
This is how I save the like state from ModelsViewModel
And in label I use
struct CellView: View{
//For CoreData
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: LikedDBE.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \LikedDBE.name, ascending: true)]) var manyLikedDB: FetchedResults<LikedDBE>
//For like
#ObservedObject var cellViewModel: ModelsViewModel = ModelsViewModel()
var body: some View{
Button(action: {
let likedDBE = LikedDBE(context: self.managedObjectContext)
likedDBE.isLiked = cellViewModel.isLiked //I indicate that isLiked from CoreData = isLiked from ModelsViewModel()
do{
cellViewModel.like() //func from ModelsViewModel()
try self.managedObjectContext.save() //Save
} catch{
print(error)
}
}, label: {
Image(systemName: cellViewModel.isLiked ? "heart.fill" : "heart") //Here I use
.frame(width: 22, height: 22)
.foregroundColor(cellViewModel.isLiked ? .red : .black) //And here I use
})
And if I use cellViewModel.isLiked, then when I click like, the like is displayed only on the one I clicked on, but the state is not saved when restarting the application, if I use likedDB.isLiked, then the like is displayed on all cells at once, but the like is saved after restarting.
I want the like to be only on the cell I clicked on and it will be saved after restarting the application.
Short answer you need something like this.
Button("add", action: {
//Create a new object
let new: LikedDBE = store.create()
//Trigger a child view that observes the object
selection = new.objectID
})
It is a button that creates the object and triggers a child view so you can observe and edit it.
Long answer will be below just copy and paste all the code into your ContentView file there are comment throughout.
import SwiftUI
import CoreData
struct ContentView: View {
#StateObject var store: CoreDataPersistence = .init()
var body: some View{
LikesListView()
//This context is aware of you are in canvas/preview
.environment(\.managedObjectContext, store.context)
}
}
struct LikesListView: View {
#EnvironmentObject var store: CoreDataPersistence
#FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \LikedDBE.name, ascending: true)]) var manyLikedDB: FetchedResults<LikedDBE>
//Control what NavigationLink is opened
#State var selection: NSManagedObjectID? = nil
var body: some View {
NavigationView{
List{
ForEach(manyLikedDB){ object in
NavigationLink(object.name ?? "no name", tag: object.objectID, selection: $selection, destination: {LikeEditView(obj: object)})
}
}.toolbar(content: {
Button("add", action: {
//Create a new object
let new: LikedDBE = store.create()
//Trigger a child view that observes the object
selection = new.objectID
})
})
}.environmentObject(store)
}
}
struct LikeEditView: View{
#EnvironmentObject var store: CoreDataPersistence
#Environment(\.dismiss) var dismiss
//Observe the CoreData object so you can see changes and make them
#ObservedObject var obj: LikedDBE
var body: some View{
TextField("name", text: $obj.name.bound).textFieldStyle(.roundedBorder)
Toggle(isOn: $obj.isLiked, label: {
Text("is liked")
})
.toolbar(content: {
ToolbarItem(placement: .navigationBarLeading){
Button("cancel", role: .destructive, action: {
store.resetStore()
dismiss()
})
}
ToolbarItem(placement: .navigationBarTrailing){
Button("save", action: {
store.update(obj)
dismiss()
})
}
})
.navigationBarBackButtonHidden(true)
}
}
struct LikesListView_Previews: PreviewProvider {
static var previews: some View {
LikesListView()
}
}
///Generic CoreData Helper not needed jsuto make stuff easy.
class CoreDataPersistence: ObservableObject{
//Use preview context in canvas/preview
//The context is for both Entities,
let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
///Non observing array of objects
func getAllObjects<T: NSManagedObject>() -> [T]{
let listRequest = T.fetchRequest()
do {
return try context.fetch(listRequest).typeArray()
} catch let error {
print ("Error fetching. \(error)")
return []
}
}
///Creates an NSManagedObject of any type
func create<T: NSManagedObject>() -> T{
T(context: context)
//Can set any defaults in awakeFromInsert() in an extension for the Entity
//or override this method using the specific type
}
///Updates an NSManagedObject of any type
func update<T: NSManagedObject>(_ obj: T){
//Make any changes like a last modified variable
save()
}
///Creates a sample
func addSample<T: NSManagedObject>() -> T{
let new: T = create()
//Can add sample data here by type checking or overriding this method
return new
}
///Deletes an NSManagedObject of any type
func delete(_ obj: NSManagedObject){
context.delete(obj)
save()
}
func resetStore(){
context.rollback()
save()
}
private func save(){
do{
try context.save()
}catch{
print(error)
}
}
}
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue
}
}
}

How can I dynamically build a View for SwiftUI and present it?

I've included stubbed code samples. I'm not sure how to get this presentation to work. My expectation is that when the sheet presentation closure is evaluated, aDependency should be non-nil. However, what is happening is that aDependency is being treated as nil, and TheNextView never gets put on screen.
How can I model this such that TheNextView is shown? What am I missing here?
struct ADependency {}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ATestView_PresentationOccursButNextViewNotShown: View {
#State private var aDependency: ADependency?
#State private var isPresenting = false
#State private var wantsPresent = false {
didSet {
aDependency = model.buildDependencyForNextExperience()
isPresenting = true
}
}
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
wantsPresent = true
}
.sheet(isPresented: $isPresenting, content: {
if let dependency = aDependency {
// Never executed
TheNextView(aDependency: dependency)
}
})
}
}
struct TheNextView: View {
let aDependency: ADependency
init(aDependency: ADependency) {
self.aDependency = aDependency
}
var body: some View {
Text("Next Screen")
}
}
This is a common problem in iOS 14. The sheet(isPresented:) gets evaluated on first render and then does not correctly update.
To get around this, you can use sheet(item:). The only catch is your item has to conform to Identifiable.
The following version of your code works:
struct ADependency : Identifiable {
var id = UUID()
}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ContentView: View {
#State private var aDependency: ADependency?
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
aDependency = model.buildDependencyForNextExperience()
}
.sheet(item: $aDependency, content: { (item) in
TheNextView(aDependency: item)
})
}
}

SwiftUI View - viewDidLoad()?

Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.

Resources