How to pass #binding variable in uivewcontroller which is hosting a SwiftUI - ios

I am hosting a SwiftUI view inside a view controller. My SwiftUI view has a #Binding data type.
While setting the content view, I need to initialize and set a binding variable.
My SwiftUI View:
struct STSChooseItemsView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var selectedImage : UIImage
var body: some View {
VStack{
HStack(){
Spacer()
.frame(width: 5)
Button(action: {
presentationMode.wrappedValue.dismiss()
self.selectedImage = UIImage()
}, label: {
Text("X")
.fontWeight(.heavy)
.foregroundColor(Color.black)
})
Spacer()
.frame(width: 100)
Text("CHOOSE ITEMS")
.font(.headline)
.padding()
Spacer()
.frame(width: 100)
}
.frame(height: 40)
Spacer()
Image(uiImage: selectedImage)
.frame(width: UIScreen.main.bounds.width - 1, height: 300)
Spacer()
}
}
My view controller:
import UIKit
import SwiftUI
class STSChooseItemsViewController : UIViewController {
let contentView = UIHostingController(rootView: STSChooseItemsView(selectedImage: **(Please let me know what and how to put data here)**))
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "CHOOSE ITEMS"
addChild(contentView)
view.addSubview(contentView.view)
setupConstraints()
// Do any additional setup after loading the view.
}
fileprivate func setupConstraints(){
contentView.view.translatesAutoresizingMaskIntoConstraints = false
contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
}
I tried initializing the dummy image but I got an error: Cannot use instance member '$myImage' within property initializer; property initializers run before 'self' is available

Try creating a Binding using the init(get:set:) initializer.
class STSChooseItemsViewController: UIViewController {
var image = UIImage()
lazy var selectedImageBinding = Binding { /// I omitted the argument label using trailing closure shorthand
return self.image
} set: { newValue in
self.image = newValue /// `selectedImageBinding` must be a lazy var to access `self`
}
lazy var contentView = UIHostingController(rootView: STSChooseItemsView(selectedImage: selectedImageBinding) /// pass in the binding here
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "CHOOSE ITEMS"
addChild(contentView)
view.addSubview(contentView.view)
setupConstraints()
// Do any additional setup after loading the view.
}
fileprivate func setupConstraints() {
contentView.view.translatesAutoresizingMaskIntoConstraints = false
contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
}

Related

Button click not working with SwiftUI added in UIKIT

I am using Main.StoryBoard with UIViewController, Now i have added SwiftUIView Inside This at top of the Storyboard as
The Code of View Controller is
import UIKit
import SwiftUI
extension UIView {
func addConstrained(subview: UIView) {
subview.translatesAutoresizingMaskIntoConstraints = false
subview.topAnchor.constraint(equalTo: topAnchor).isActive = true
subview.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
subview.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
subview.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
}
class ViewController: UIViewController {
#IBOutlet weak var topNavView: UIView!
let childView = UIHostingController(rootView: TempView())
override func viewDidLoad() {
super.viewDidLoad()
childView.view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 152)
addChild(childView)
topNavView.addSubview(childView.view)
childView.didMove(toParent: self)
topNavView.addConstrained(subview: childView.view)
childView.view.isUserInteractionEnabled = true
}
}
The swiftUI View is---
import SwiftUI
struct TempView: View {
var body: some View {
ZStack {
Color.green
VStack {
Button {
print("clicked 1")
} label: {
Text("Button1")
}
.frame(width: 70, height: 60, alignment: .center)
Button {
print("clicked 2")
} label: {
Text("Button2")
}
.frame(width: 70, height: 60, alignment: .center)
}
}
//.background(.red)
.frame(height:100)
}
}
struct TempView_Previews: PreviewProvider {
static var previews: some View {
TempView()
}
}
When i click on Button1 & Button2 I am not able to click. but when i rotate device (Landscape ) it's working fine.. what could be the issue ?

how to pass an img value between 2 views?

I am trying to pass a #Binding UI Image from an Image picker to another view through a sheet. I tried to use the OnDismiss() parameter but it did not work, even tried ondisapper(). What i want is that, when i press a button, it should open up the image picker directly. When a user has selected an image it should take that image and navigate to another view and show the image. This is the code im trying but is not working : -
NavigationBar.swift
import SwiftUI
struct NavigationBar: View {
var title = ""
#Binding var hasScrolled: Bool
#State var showUploadPost = false
#State var imagePickerPresented = false
#State private var selectedImage: UIImage?
#State var postImage: Image?
// #State var showAccount = false
#State var captionText = ""
var body: some View {
ZStack {
Color.clear
.background(.ultraThinMaterial)
.blur(radius: 10)
.opacity(hasScrolled ? 1 : 0)
Text(title)
.animatableFont(size: hasScrolled ? 22 : 34, weight: .bold)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 20)
.padding(.top, 20)
.offset(y: hasScrolled ? -4 : 0)
HStack(spacing: 16) {
Button {
imagePickerPresented = true
} label: {
Image(systemName: "plus.square")
.font(.title3.weight(.bold))
.frame(width: 46, height: 46)
.foregroundColor(.secondary)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
.strokeStyle(cornerRadius: 14)
}
.sheet(isPresented: $imagePickerPresented, onDismiss: {
newPostView(image: $selectedImage) // this is the problem
}) {
ImagePicker(image: $selectedImage)
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 20)
.padding(.top, 20)
.offset(y: hasScrolled ? -4 : 0)
}
.frame(height: hasScrolled ? 44 : 70)
.frame(maxHeight: .infinity, alignment: .top)
}
}
extension NavigationBar {
func loadImage() {
guard let selectedImage = selectedImage else { return }
postImage = Image(uiImage: selectedImage)
}
}
newPostView.swift
import SwiftUI
struct newPostView: View {
#Binding var image: UIImage?
var body: some View {
VStack {
Text("hi")
}
}
}
ImagePicker.swift
struct ImagePicker: UIViewControllerRepresentable {
#Binding var image: UIImage?
#Environment(\.presentationMode) var mode
func makeUIViewController(context: Context) -> some UIViewController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.originalImage] as? UIImage else { return }
self.parent.image = image
self.parent.mode.wrappedValue.dismiss()
}
}
}
Solution 1:
Simply use hidden navigation bar technique to push it to another view on selecting the Image.
struct NavigationBar: View {
#State var imagePickerPresented = false
#State private var selectedImage: UIImage?
#State private var newPost = false
var body: some View {
VStack {
Text("Hello, World!")
Button {
imagePickerPresented = true
} label: {
Image(systemName: "plus.square")
.font(.title3.weight(.bold))
.frame(width: 46, height: 46)
.foregroundColor(.secondary)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
}
.sheet(isPresented: $imagePickerPresented, onDismiss: {
newPost.toggle() //1 <--- FIX
}) {
ImagePicker(image: $selectedImage)
}
// 2 FIX ⬇️
// Hide Navigation Link
NavigationLink("NewPostView", isActive: $newPost) {
newPostView(image: $selectedImage)
}.hidden()
}
}
}
Note: This works when the NavigationBar is inside the NavigationView.
For example -
struct ContenView: View {
var body: some View {
NavigationView{
NavigationBar()
}
}
}
Solution 2: If you don't want Navigation to the newPostView, use like this
struct NavigationBar: View {
#State var imagePickerPresented = false
#State private var selectedImage: UIImage?
#State private var newPost = false
var body: some View {
VStack {
Text("Hello, World!")
Button {
imagePickerPresented = true
} label: {
Image(systemName: "plus.square")
.font(.title3.weight(.bold))
.frame(width: 46, height: 46)
.foregroundColor(.secondary)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
}
.sheet(isPresented: $imagePickerPresented, onDismiss: {
newPost.toggle() //1 <--- FIX
}) {
ImagePicker(image: $selectedImage)
}
.fullScreenCover(
isPresented: $newPost) {
newPostView(image: $selectedImage)
}
}
}
}
struct ContentView: View {
#State var imagePickerPresented = false
#State private var selectedImage: UIImage?
#State private var newPost = false
var body: some View {
NavigationBar()
}
}

How to clear text in custom UITextView when a send button is clicked in a SwiftUI Chat app?

I have limited knowledge of UIKit. So I followed a tutorial on creating a chat room. Following the code. The only problem is that I can not reset the chat text after sending the message. The text message is getting reset. But it is not reflecting inside Custom UITextView called MessageField. I am using Xcode 12.3.
How can I clear UITextView's text when the send button action is executed?
MessageField.swift
import SwiftUI
struct MessageField: UIViewRepresentable {
#Binding var text: String
#Binding var height: CGFloat
var placeholder = "Message"
func makeCoordinator() -> Coordiator {
return MessageField.Coordiator(parent: self, placeholder: placeholder)
}
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isEditable = true
view.isScrollEnabled = true
view.text = placeholder
view.font = .systemFont(ofSize: 18)
view.textColor = .lightGray
view.backgroundColor = .clear
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
DispatchQueue.main.async {
self.height = uiView.contentSize.height
}
}
class Coordiator: NSObject, UITextViewDelegate {
var parent: MessageField
var placeholder: String
init(parent: MessageField, placeholder: String) {
self.parent = parent
self.placeholder = placeholder
}
func textViewDidBeginEditing(_ textView: UITextView) {
if self.parent.text == "" {
textView.text = ""
textView.textColor = .label
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if self.parent.text == "" {
textView.text = placeholder
textView.textColor = .lightGray
}
}
func textViewDidChange(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.height = textView.contentSize.height
self.parent.text = textView.text
}
}
}
}
ChatField.swift
import SwiftUI
struct ChatField: View {
#Binding var text: String
#State var height: CGFloat = 0
var sendText: () -> ()
var body: some View {
HStack(alignment: .bottom) {
MessageField(text: self.$text, height: self.$height)
.frame(height: self.height < 150 ? self.height : 150)
.padding(.horizontal)
.background(Color(.systemBackground))
.cornerRadius(10)
Button(action: {
self.sendText()
self.text = ""
print("Message sent")
}, label: {
Image(systemName: "paperplane.fill")
.resizable()
.scaledToFit()
.padding(12)
.frame(height: 38, alignment: .center)
.foregroundColor(.foregroundLabel)
})
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.padding(.horizontal)
.padding(.vertical, 5)
}
}
ChatRoom.swift
import SwiftUI
struct ChatRoom: View {
#State var text = ""
#State var height: CGFloat = 0
var body: some View {
VStack(spacing: 0) {
HStack {
Text("Chat Room")
.font(.title)
.fontWeight(.bold)
Spacer()
}
.padding()
.background(Color(.systemBackground))
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
ForEach(0 ..< 60) { item in
Text("Hello \(item+1)")
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity, alignment: .leading)
}
ChatField(text: $text, height: height) {
print("Sending following text:")
print(self.text)
}
.background(Color(.systemBackground).edgesIgnoringSafeArea(.bottom))
}
.background(Color(.systemGroupedBackground))
.onTapGesture {
UIApplication.shared.windows.first?.rootViewController?.view.endEditing(true)
}
}
}
The UIViewRepresentable.updateUIView is called when binding is modified, so you need to apply externally modified data (text in your case) in this function, like
func updateUIView(_ uiView: UITextView, context: Context) {
if self.text != "" { uiView.text = self.text }
DispatchQueue.main.async {
self.height = uiView.contentSize.height
}
}
The logic used in the tutorial have nice UI. But practically not usable. The UI text was updated from textViewDidBeginEditing and textViewDidEndEditing directly. This logic created so many problems while creating complete chat system. After lots of trail and error to understand the working and debugging, I found a perfect straight forward solution.
Solution:
Use a isEditing boolean to track changes and update UI only inside updateUIView method.
That fixed everything. Following is the fixed code.
MessageField.swift
import SwiftUI
struct MessageField: UIViewRepresentable {
#Binding var text: String
#Binding var height: CGFloat
var placeholder: String
#State var isEditing: Bool = false
func makeCoordinator() -> Coordiator {
return MessageField.Coordiator(parent: self)
}
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isEditable = true
view.isScrollEnabled = true
view.text = placeholder
view.font = .systemFont(ofSize: 18)
view.textColor = .lightGray
view.backgroundColor = .clear
view.delegate = context.coordinator
return view
}
func updateUIView(_ textView: UITextView, context: Context) {
if self.text == "" {
textView.text = self.isEditing ? "" : self.placeholder
textView.textColor = self.isEditing ? .label : .lightGray
}
DispatchQueue.main.async {
self.height = textView.contentSize.height
}
}
class Coordiator: NSObject, UITextViewDelegate {
var parent: MessageField
init(parent: MessageField) {
self.parent = parent
}
func textViewDidBeginEditing(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.isEditing = true
}
}
func textViewDidEndEditing(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.isEditing = false
}
}
func textViewDidChange(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.height = textView.contentSize.height
self.parent.text = textView.text
}
}
}
}

Lottie animation disappears on last TabView page in SwiftUI

I have a TabView with six pages. The animation is in the last page.
When I attend to the last page the animation shows for a split second and disappear completely.
Thought it might be problem with the animation but it works elsewhere just fine.
I present this TabView using sheet.
Last page:
struct SixScreen: View{
#EnvironmentObject var session: SessionStore
#Binding var dismiss: Bool
var body: some View{
VStack(spacing: 16){
Spacer()
LottieView(name: "complete")
.frame(width: 200, height: 200, alignment: .center)
Button(action: {
dismiss.toggle()
}, label: {
Text("Start")
.frame(width: 100, height: 50, alignment: .center)
.foregroundColor(.blue)
.background(Color.white)
.cornerRadius(10)
.shadow(color: .blue, radius: 5, x: 0, y: 1)
})
.padding(.bottom, 32)
Spacer()
}
}
}
Lottie View implementation:
struct LottieView: UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
var name: String!
var animationView = AnimationView()
class Coordinator: NSObject {
var parent: LottieView
init(_ animationView: LottieView) {
self.parent = animationView
super.init()
}
}
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView()
animationView.animation = Animation.named(name)
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .loop
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
animationView.play()
}
}
Tab View:
Group{
TabView{
FirstScreen()
SecondScreen()
ThirdScreen()
FourthScreen()
FifthScreen()
SixScreen(dismiss: $dismiss)
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.padding(.bottom)
}
.background(gradient)
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(true)
}
Try to force refresh the view using the .id() modifier
struct AnimationView: View {
#State private var lottieID = UUID()
var body: some View {
LottieView(name: "complete")
.frame(width: 200, height: 200, alignment: .center)
.id(lottieID)
.onAppear {
lottieID = UUID()
}
}
}
After a lot of research and testing, if you're looking to keep the Lottie animation live inside the SwiftUI TabView you should add this code snippet:
public func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView(frame: .zero)
let animation = Animation.named(lottieFile)
animationView.animation = animation
animationView.animationSpeed = animationSpeed
animationView.contentMode = .scaleAspectFit
animationView.loopMode = loopMode
animationView.backgroundBehavior = .pauseAndRestore <------

Transparent navigationbar in SwiftUI with backgroundImage

I have a custom navigation bar that is the an image followed by the header text. The image is set to scale to fill but does not quite fill up the navigation bar. As such, you can see a little of the bar where the image does not cover. I've tried to set the background color of the nav bar to clear but that doesn't work. Any suggestions?
struct ContentView: View {
#State private var hideBar = true
var body: some View {
VStack {
NavigationView {
ZStack {
Image("bg5").resizable().scaledToFill()
VStack {
NavigationLink(destination:
SubView(header: "Go to subview")) {
Text("Go to subview")
}.simultaneousGesture(TapGesture().onEnded {
self.hideBar = false // << show, here to be smooth !!
})
NavigationLink(destination:
SubView(header: "Go again")) {
Text("Go to subview again")
}.simultaneousGesture(TapGesture().onEnded {
self.hideBar = false // << show, here to be smooth !!
})
}
.navigationBarTitle("")
.navigationBarHidden(hideBar)
.onAppear {
self.hideBar = true // << hide on back
}
}.edgesIgnoringSafeArea([.top, .bottom])
}
}
}
}
struct SubView: View {
var header: String
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var btnBack : some View { Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("subheaderback").resizable().scaledToFit()
VStack(alignment: .center) {
Text(self.header)
}.frame(width: 100, height: 100)
}
}.buttonStyle(PlainButtonStyle())
}
var body: some View {
ZStack {
Image("bg5").resizable().scaledToFill()
VStack {
Text("blah blah")
Text("and more blah")
}
}.edgesIgnoringSafeArea([.top, .bottom])
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}
extension UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .clear
appearance.backgroundImage = UIImage(named: "subheader")
navigationBar.standardAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
}
}
Instead of setting background color you need different configuration, like below (tested with Xcode 11.4 / iOS 13.4)
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground() // << this one !!
appearance.backgroundImage = UIImage(named: "subheader")
// appearance.backgroundImageContentMode = .scaleAspectFit // if needed non-default

Resources