Change view property from another View SwiftUI - swiftui

I'm trying to understand SwiftUI. And can't understand why so easy staffs from uikit now extremely hard.
For example:
I've got custom button:
public struct PrimaryButton: View {
private var title: String
private var didButtonTapped: ()->Void
private var icon: Image?
#State var loading: Bool = true
public init(title: String, icon: Image? = nil, didButtonTapped: #escaping ()->Void) {
self.title = title
self.icon = icon
self.didButtonTapped = didButtonTapped
}
public var body: some View {
Button {
didButtonTapped()
} label: {
if loading {
ProgressView()
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
Text(title)
.minimumScaleFactor(0.1)
.font(.headline)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
}
I've added it to another view. And want to change "loading" property. Should I create new property in the new view?
But what if I will had 10 btns? Should I create 10 different properties for 10 different buttons? Or how I can get access?
One way that I've found - replace:
#State var loading: Bool = true
with
#Binding var loading: Bool
and pass it to constructor.
Is it right way? Views now can't be modified like in UIKit? ex:
privaryBtn.loading = true
?
Thanks a lot for help, mates.

Related

How to make a custom UIView Appear/Dissapear in SwiftUI

I have a CameraView in my app that I'd like to bring up whenever a button is to be presssed. It's a custom view that looks like this
// The CameraView
struct Camera: View {
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraViewfinder(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// Buttons and controls on top of the CameraViewfinder
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
// More view component code goes here but I've excluded it all for brevity (they don't add anything substantial to the question being asked.
} // [End of CameraView]
It contains a CameraViewfinder View which conforms to the UIViewRepresentable Protocol:
struct CameraViewfinder: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
I wish to add a binding property to this camera view that allows me to toggle this view in and out of my screen like any other social media app would allow. Here's an example
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
I understand that the code to achieve this must be written inside the updateUIView() method. Now, although I'm quite familiar with SwiftUI, I'm relatively inexperienced with UIKit, so any help on this and any helpful resources that could help me better code situations similar to this would be greatly appreciated.
Thank you.
EDIT: Made it clear that the first block of code is my CameraView.
EDIT2: Added Example of how I'd like to use the CameraView in my App.
Judging by the way you would like to use it in the app, the issue seems to not be with the CameraViewFinder but rather with the way in which you want to present it.
A proper SwiftUI way to achieve this would be to use a sheet like this:
#State var showCamera: Bool = false
var body: some View {
mainTabView
.sheet(isPresented: $showCamera) {
CameraView()
.interactiveDismissDisabled() // Disables swipe to dismiss
}
}
If you don't want to use the sheet presentation and would like to cover the whole screen instead, then you should use the .fullScreenCover() modifier like this.
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView()
.fullScreenCover(isPresented: $showCamera)
}
}
Either way you would need to somehow pass the state to your CameraView to allow the presented screen to set the state to false and therefore dismiss itself, e.g. with a button press.

How to make a left vertical tabView on SwiftUI TVOS?

Whenever a tabButton is highlighted, I wanna show the corresponding content on the RightMainView, I can use a #Published property on ViewModel to do that, but the problem is that the same RightMainView will be redraw while switching tabs.
The MainView will be a complicated UI and also has focus engine, so I definitely do not want the MainView redraw.
import SwiftUI
struct Model: Identifiable, Equatable {
let id = UUID()
let title: String
static func == (lhs: Model, rhs: Model) -> Bool {
return lhs.title == rhs.title
}
}
class ViewModel: ObservableObject {
let titles = [Model(title: "Home"), Model(title: "live"), Model(title: "setting"), Model(title: "network")]
#Published var selected: Model = Model(title: "Home")
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
HStack(spacing: 0) {
leftTab
.focusSection()
rightMainView
}.edgesIgnoringSafeArea(.all)
}
private var leftTab: some View {
VStack {
ForEach(viewModel.titles, id: \.self.id) { title in
ZStack {
TabButton(viewModel: viewModel, title: title)
}.focusable()
}
}
.frame(maxWidth: 400, maxHeight: .infinity)
.background(.yellow)
}
private var rightMainView: some View {
VStack {
let _ = print("Redrawing the View")
Text(viewModel.selected.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
}
}
}
struct TabButton: View {
#Environment(\.isFocused) var isFocused
let viewModel: ViewModel
let title: Model
var body: some View {
Text(title.title)
.frame(width: 200)
.padding(30)
.background(isFocused ? .orange : .white)
.foregroundColor(isFocused ? .black : .gray)
.cornerRadius(20)
.onChange(of: isFocused) { newValue in
if newValue {
viewModel.selected = title
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here are some other things I have tried:
I tried the native TabView, https://developer.apple.com/documentation/swiftui/tabview, the View does not redrew while switch tabs, but it only support horizontal, not left vertical model, is there a way I can use native TabView to implement my vertical tabView (with both text and images)
I tried the NavigationSplitView as well, https://developer.apple.com/documentation/swiftui/navigationsplitview, the behavior is not the same on TVOS with iPad tho. On TVOS only the TabButtons are showing, the MainView/details are not showing

SwiftUI AppStorage, UserDefaults and ObservableObject

I have two different views, ContentView and CreateView.
In CreateView, I get user's inputs by textfield, and once user clicks on Save button, the inputs will be stored in AppStorage.
Then, I want to display the saved inputs on ContentView.
Here, I tried to use State & Binding but it didn't work out well.
How would I use the variable, that is created in CreateView, in ContentView?
what property should I use..
Thanks
Here's the updated questions with the code...
struct ContentView: View {
// MARK: - PROPERTY
#ObservedObject var appData: AppData
let createpage = CreatePage(appData: AppData())
var body: some View {
HStack {
NavigationLink("+ create a shortcut", destination: CreatePage(appData: AppData()))
.padding()
Spacer()
} //: HStack - link to creat page
VStack {
Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
}
}
struct CreatePage: View {
// MARK: - PROPERTY
#AppStorage("title") var currentShortcutTitle: String?
#AppStorage("option") var currentOption: String?
#Environment(\.presentationMode) var presentationMode
#ObservedObject var appData: AppData
var body: some View {
NavigationView{
ScrollView{
Text("Create a ShortCut")
.padding()
HStack {
TextField("what is the title?", text: $appData.titleInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
//.frame(width: 150, height: 60, alignment: .center)
.border(Color.black)
.padding()
} //: HStack - Textfield - title
.padding()
HStack (spacing: 10) {
TextField("options?", text: $appData.optionInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80, height: 40, alignment: .leading)
.padding()
} //: HStack - Textfield - option
.padding()
Button(action: {
self.appData.shortcutTitle = self.appData.titleInput
self.appData.shortcutOption = self.appData.optionInput
UserDefaults.standard.set(appData.shortcutTitle, forKey: "title")
UserDefaults.standard.set(appData.shortcutOption, forKey: "option")
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
.padding()
.frame(width: 120, height: 80)
.border(Color.black)
}) //: Button - save
.padding(.top, 150)
} //: Scroll View
} //: Navigation View
} //: Body
class AppData: ObservableObject {
#Published var shortcutTitle : String = "Deafult Shortcut"
#Published var shortcutOption : String = "Default Option"
#Published var titleInput : String = ""
#Published var optionInput : String = ""
}
So the problem here is that
when I put new inputs on CreatePage and tab the save button, the new inputs do not appear on ContentView page.The output keeps showing the default values of title and option, not user inputs.
If user makes a new input and hit the save button, I want to store them in AppStorage, and want the data to be kept on ContentView (didn't make the UI yet). Am I using the AppStorage and UserDefaults in a right direction?
If anyone have insights on these issues.. would love to take your advice or references.
You're creating instances of AppData in multiple places. In order to share data, you have to share one instance of AppData.
I'm presuming that you create AppData in a parent view of ContentView since you have #ObservedObject var appData: AppData defined at the top of the view (without = AppData()). This is probably in your WindowGroup where you also must have a NavigationView.
I removed the next (let createpage = CreatePage(appData: AppData())) because it does nothing. And in the NavigationLink, I passed the same instance of AppData.
struct ContentView: View {
// MARK: - PROPERTY
#StateObject var appData: AppData = AppData() //Don't need to have `= AppData()` if you already create it in a parent view
var body: some View {
// I'm assuming there's a NavigationView in a parent view
VStack { //note that I've wrapped the whole view in a VStack to avoid having two root nodes (which can perform differently in NavigationView depending on the platform)
HStack {
NavigationLink("+ create a shortcut", destination: CreatePage(appData: appData))
.padding()
Spacer()
} //: HStack - link to creat page
VStack {
Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
}
}
}
}
struct CreatePage: View {
// MARK: - PROPERTY
#AppStorage("title") var currentShortcutTitle: String?
#AppStorage("option") var currentOption: String?
#Environment(\.presentationMode) var presentationMode
#ObservedObject var appData: AppData
var body: some View {
NavigationView{
ScrollView{
Text("Create a ShortCut")
.padding()
HStack {
TextField("what is the title?", text: $appData.titleInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
//.frame(width: 150, height: 60, alignment: .center)
.border(Color.black)
.padding()
} //: HStack - Textfield - title
.padding()
HStack (spacing: 10) {
TextField("options?", text: $appData.optionInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80, height: 40, alignment: .leading)
.padding()
} //: HStack - Textfield - option
.padding()
Button(action: {
appData.shortcutTitle = appData.titleInput
appData.shortcutOption = appData.optionInput
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
.padding()
.frame(width: 120, height: 80)
.border(Color.black)
}) //: Button - save
.padding(.top, 150)
} //: Scroll View
} //: Navigation View
} //: Body
}
Regarding #AppStorage and UserDefaults, it's a little hard to tell what your intent is at this point with those. But, you shouldn't need to declare AppStorage and call UserDefaults on the same key -- #AppStorage writes to UserDefaults for you. Read more at https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper
you can use a singleton ObservableObject that conforms to NSObject so you can observe everything even older apple objects like progress.
class appData : NSObject , ObservableObject {
static let shared = appData()
#Published var localItems = Array<AVPlayerItem>()
#Published var fractionCompleted : Double = 0
#Published var downloaded : Bool = false
#Published var langdentifier = UserDefaults.standard.value(forKey: "lang") as? String ?? "en" {
didSet {
print("AppState isLoggedIn: \(langIdentifier)")
}
}
var progress : Progress?
override init() {
}
}
then u can use it anywhere in your code like this:
appData.shared.langIdentifier == "en" ? .leading : .trailing
You should be able to simply put AppStorage objects in the ObservableClass and call them from there. There should be no need to put AppStorage in the View and then read from UserDefaults in the class.
class AppData: ObservableObject {
#AppStorage(keyForValue) var valueForKey: ValueType = defaultValue
...
}
Of course, you could make #Published property in the class and define the getter and setter for it so it reads and writes directly to the UserDefaults but at that point, you're just creating more work than simply using AppStorage from the beginning directly in the class.

SwiftUI TextEditor - save the state after completion of editing

Overview of the issue
I am building a note taking app and there is a note editing view where a few TextEditors are used to listen to users' input. Right now an environment object is passed to the this note editing view and I designed a save button to save the change. It works well except that users have to click the save button to update the model.
Expected behaviors
The text editor is expected to update the value of instances of the EnvironmentObject once the editing is done. Do not necessarily click the save button to save the changes.
below is the sample code of view
struct NoteDetails: View {
#EnvironmentObject var UserData: Notes
#Binding var selectedNote: SingleNote?
#Binding var selectedFolder: String?
#Binding var noteEditingMode: Bool
#State var title:String = ""
#State var updatedDate:Date = Date()
#State var content: String = ""
var id: Int?
var label: String?
#State private var wordCount: Int = 0
var body: some View {
VStack(alignment: .leading) {
TextEditor(text: $title)
.font(.title2)
.padding(.top)
.padding(.horizontal)
.frame(width: UIScreen.main.bounds.width, height: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
Text(updatedDate, style: .date)
.font(.subheadline)
.padding(.horizontal)
Divider()
TextEditor(text: $content)
.font(.body)
.lineSpacing(15)
.padding(.horizontal)
.frame(maxHeight:.infinity)
Spacer()
}
}
}
below is the sample code of edit func of the EnvironmentObject
UserData.editPost(label: label!, id: id!, data: SingleNote(updateDate: Date(), title: title, body: content))
Ways I have tried
I tried to add a onChange modifier to the TextEditor but it applies as soon as any change happens, which is not desired. I also tried a few other modifiers like onDisapper etc.
User data has #Published var NoteList: [String: [SingleNote]] and I tried to pass the $UserData.NoteList[label][id].title in to the TextEditor and it was not accepted either
Did not find sound solutions in the Internet so bring up this question here. Thanks for suggestions in advance!
I don't know exactly what you mean to save once the editing is done. Here are two possible approaches I found.
Note:
In the following demos, the text with blue background displays the saved text.
1. Saving when user dismisses keyboard
Solution: Adding a tap gesture to let users dismiss the keyboard when tapped outside of the TextEditor. Call save() at the same time.
Code:
struct ContentView: View {
#State private var text: String = ""
#State private var savedText: String = ""
var body: some View {
VStack(spacing: 20) {
Text(savedText)
.frame(width: 300, height: 200, alignment: .topLeading)
.background(Color.blue)
TextEditor(text: $text)
.frame(width: 300, height: 200)
.border(Color.black, width: 1)
.onTapGesture {}
}
.onTapGesture { hideKeyboardAndSave() }
}
private func hideKeyboardAndSave() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
save()
}
private func save() {
savedText = text
}
}
2. Saving after no changes for x seconds
Solution: Using Combine with .debounce to publish and observe only after x seconds have passed with no further events.
I have set x to 3.
Code:
struct ContentView: View {
#State private var text: String = ""
#State private var savedText: String = ""
let detector = PassthroughSubject<Void, Never>()
let publisher: AnyPublisher<Void, Never>
init() {
publisher = detector
.debounce(for: .seconds(3), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
var body: some View {
VStack(spacing: 20) {
Text(savedText)
.frame(width: 300, height: 200, alignment: .topLeading)
.background(Color.blue)
TextEditor(text: $text)
.frame(width: 300, height: 200)
.border(Color.black, width: 1)
.onChange(of: text) { _ in detector.send() }
.onReceive(publisher) { save() }
}
}
private func save() {
savedText = text
}
}

How to make a Dynamic PageViewController in SwiftUI?

Anyone know how to make a dynamic pageView controller in SwiftUI, iOS 14? Something that displays pages that are a function of their date so that one can scroll left or right to look at data from the past, present and future.
struct DatePg: View
{
let date: Date
var body: some View {
Text(date.description)
}
}
There is a new API that allows one to make a PageViewController with the TabView and a viewModifier. But the only examples I've seen are static. Here's an example of a static PageView.
import SwiftUI
struct SwiftUIPageView: View
{
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
Text("Hello")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.tag(0)
Text("World")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tag(1)
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
Already have something working using UIHostingController but passing NSManageObjectContext through the UIKit objects is cuasing problems.
Here's where I'm at so far. Still not working.
import SwiftUI
#main struct PagerApp: App
{
var body: some Scene {
WindowGroup { DatePageView() }
}
}
struct DatePageView: View
{
#StateObject var dateData = DateData(present: Date())
#State var index: Int = 1
var body: some View {
TabView(selection: $index) {
ForEach(dateData.dates, id: \.self) { date in
Text(date.description)
.onAppear { dateData.current(date: date) }
.tag(dateData.tag(date: date))
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
class DateData: ObservableObject
{
#Published var dates: [Date]
init(present: Date) {
let past = present.previousDay()
let future = present.nextDay()
self.dates = [past, present, future]
}
func current(date: Date) {
//center around
guard let i = dates.firstIndex(of: date) else { fatalError() }
self.dates = [ dates[i].previousDay(), dates[i], dates[i].nextDay() ]
print("make item at \(i) present")
}
func tag(date: Date) -> Int {
guard let i = dates.firstIndex(of: date) else { fatalError() }
return i
}
}
You can create view dynamically using an array and ForEach.
Here is an example using an array of strings:
// See edited section
You could pass the items you want in the View initializer
Edit:
Here is an example for adding a new page each time I reach the last one:
struct SwiftUIPageView: View
{
#State private var selection = "0"
#State var items: [String] = ["0", "1", "2", "3"]
var body: some View {
TabView(selection: $selection) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tag(item)
}
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onChange(of: selection, perform: { value in
if Int(value) == (self.items.count - 1) {
self.items.append("\(self.items.count)")
}
})
.id(items)
}
}
The last id(items) is important because it forces the View to reload when the array changes.