Swiftui Why is background not working until second row is tapped - ios

I have a share view controller attached to a list . When a user taps on a list row the ShareView is supposed to pop and display the contents of the list that was tapped . The issue is when I start the app and tap on a list item the ShareView text is blank, if I tap on a second different item the it shows the content:
// example : start the app, click List 1 and see that no content displays, then Click List 2 and content is displayed .
How can I make it so that the first time you tap a list the content is displayed in ShareView controller . This is the small project
import SwiftUI
struct ContentView: View {
let StringList = ["List 1","List 2","List 3","List 4","List 5"]
#State var TextExample = ""
#State var IsOpen = false
var body: some View {
List {
ForEach(StringList, id: \.self) { string in
Text(string)
.onTapGesture {
TextExample = string
IsOpen = true
}
}
}.background(SharingViewController(isPresenting: $IsOpen) {
print("\(TextExample)")
let av = UIActivityViewController(activityItems: [TextExample], applicationActivities: nil)
av.completionWithItemsHandler = { _, _, _, _ in
IsOpen = false // required for re-open !!!
}
return av
})
}
}
struct SharingViewController: UIViewControllerRepresentable {
#Binding var isPresenting: Bool
var content: () -> UIViewController
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if isPresenting {
uiViewController.present(content(), animated: true, completion: nil)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any suggestions would be great

Im not sure if this is an old bug, but I saw this before.
However, I found that using #StateObject class and its sub variable #Published to bind the data instead of local #State variable solved this problem.
Below is the working code which I tested. Also, here is the proof video link: https://drive.google.com/file/d/1ItlOf33vasO9WFRDokUq_PXLzUpR8UBa/view?usp=sharing
class VM : ObservableObject {
#Published var storedText = "" //added
}
struct ContentView: View {
#StateObject var viewModel = VM() //added
let StringList = ["List 1","List 2","List 3","List 4","List 5"]
#State var TextExample = ""
#State var IsOpen = false
var body: some View {
List {
ForEach(StringList, id: \.self) { string in
Text(string)
.onTapGesture {
viewModel.storedText = string //added
IsOpen = true
}
}
}.background(
SharingViewController(isPresenting: $IsOpen) {
print("\(viewModel.storedText)") //added
let av = UIActivityViewController(activityItems: [viewModel.storedText], applicationActivities: nil) //added
av.completionWithItemsHandler = { _, _, _, _ in
IsOpen = false // required for re-open !!!
}
return av
})
}
}
struct SharingViewController: UIViewControllerRepresentable {
#Binding var isPresenting: Bool
var content: () -> UIViewController
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if isPresenting {
uiViewController.present(content(), animated: true, completion: nil)
}
}
}

Related

How to have a Published var of a view type with a view builder?

I have this class:
final class DialogPresentation: ObservableObject {
#Published var isPresented = false
#Published var dialogContent: DialogContent?
func show(content: DialogContent?) {
if let presentDialog = content {
dialogContent = presentDialog
isPresented = true
} else {
isPresented = false
}
}
}
This is what DialogContent looks like:
enum DialogContent: View {
case contentDetail01(isPresented: Binding<Bool>, title: String)
case contentDetail02(isPresented: Binding<Bool>)
#ViewBuilder
var body: some View {
switch self {
case .contentDetail01(let isPresented, let title):
DemoDialogContent01(isPresented: isPresented, title: title)
case .contentDetail02(let isPresented):
DemoDialogContent02(isPresented: isPresented)
}
}
}
When I try to change DialogContent to this:
struct DynamicDialogView<Content: View>: View {
#Binding var isPresented: Bool
let dialogContent: Content
init(isPresented: Binding<Bool>,
#ViewBuilder dialogContent: () -> Content) {
_isPresented = isPresented
self.dialogContent = dialogContent()
}
I am getting a lot of issues on DialogPresentation.
Any suggestions?

Warning when presenting UIActivityViewController inside a SwiftUI sheet

I use UIActivityViewController to allow my SwiftUI app to share content with other activities:
import Foundation
import SwiftUI
struct ShareSheet : UIViewControllerRepresentable{
var items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
let ac = UIActivityViewController(activityItems: items, applicationActivities: nil)
return ac
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
}
}
To make it clear, I will use the following simple code:
import SwiftUI
struct ContentView: View {
#State var showForImageShare: Bool = false
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
.onTapGesture {
showForImageShare.toggle()
}
}
.sheet(isPresented: $showForImageShare){
let image = "Hello everybody"
ShareSheet(items: [image])
}
}
}
I got the following warning in the console:
2022-10-23 23:41:07.692216+0300 Test UIActivityViewController[92164:30374157] [LayoutConstraints] Changing the translatesAutoresizingMaskIntoConstraints property of a UICollectionReusableView that is managed by a UICollectionView is not supported, and will result in incorrect self-sizing. View: <_UIActivityContentFooterView: 0x15cd56340; baseClass = UICollectionReusableView; frame = (16 244.333; 361 52); layer = <CALayer: 0x600001ab8180>>
I use Xcode 14, Deployment Target: 16.0
Could you please help me to find the best way to present UIActivityViewController ?
How about this?
import SwiftUI
struct ContentView: View {
#State private var isActivityViewPresented = false
var body: some View {
Button("Share Link") {
self.isActivityViewPresented = true
}
.background(
ActivityView(
isPresented: $isActivityViewPresented,
activityItmes: [URL(string: "https://stackoverflow.com/questions/74174661/warning-when-presenting-uiactivityviewcontroller-inside-a-swiftui-sheet")!]
)
)
}
}
public struct ActivityView: UIViewControllerRepresentable {
#Binding var isPresented: Bool
public let activityItmes: [Any]
public let applicationActivities: [UIActivity]? = nil
public func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
public func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
let activityViewController = UIActivityViewController (
activityItems: activityItmes,
applicationActivities: applicationActivities
)
if isPresented && uiViewController.presentedViewController == nil {
uiViewController.present(activityViewController, animated: true)
}
activityViewController.completionWithItemsHandler = { (_, _, _, _) in
isPresented = false
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Unable to access variable values in view

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)
}
}
}
}
}

Callback with passing data from SwiftUI to UIKit

How can I send data from SwiftUI view to UIKit ViewController in callback's closure?
Let's say we have SwiftUI View:
import SwiftUI
struct MyView: View {
var buttonPressed: (() -> Void)?
#State var someData = ""
var body: some View {
ZStack {
Color.purple
Button(action: {
someData = "new Data"
self.buttonPressed?()
}) {
Text("Button")
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
And ViewController where, inside which we have SwiftUI view:
import UIKit
import SwiftUI
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let swiftUIView = MyView()
let hostingViewController = UIHostingController(rootView: swiftUIView)
self.view.addSubview(hostingViewController.view)
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
hostingViewController.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
hostingViewController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
hostingViewController.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
hostingViewController.view.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
hostingViewController.rootView.buttonPressed = {
print ("callback recived")
// i know i can try to get the data in this way, but if MyView become too complex than it won't look well
//print(hostingViewController.rootView.$someData)
}
}
}
How can I send someData via closure to ViewController?
You can pass it via argument, like
struct MyView: View {
var buttonPressed: ((String) -> Void)? // << here !!
#State var someData = ""
var body: some View {
ZStack {
Color.purple
Button(action: {
someData = "new Data"
self.buttonPressed?(someData) // << here !!
and
hostingViewController.rootView.buttonPressed = { value in // << here !!
print("callback received")
print(value)
}

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)
})
}
}

Resources