So I have an app like the landmark app from apple.
I'm trying to figure out how to save the favorites, but I'm struggling with it.
I tried with save() and with userDefaults but with no result.
Some solutions use Toggle(isOn:), but the code doesn't use this.
The code from the landmark app is:
Button(action: {
self.userData.landmarks[self.landmarkIndex]
.isFavorite.toggle()
}) {
if self.userData.landmarks[self.landmarkIndex]
.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
} else {
Image(systemName: "star")
.foregroundColor(Color.gray)
}
}
One of my attempts was the following:
Button(action: {
UserDefaults.standard.set(self.userData.landmarks[self.landmarkIndex]
.isFavorite.toggle(self.isFavorite, forKey: Bool)
)
}) {
if UserDefaults.standard.set(self.userData.landmarks[self.landmarkIndex])
.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
} else {
Image(systemName: "star")
.foregroundColor(Color.gray)
}
I get an error 'Type of expression is ambiguous without more context'.
The button action is embedded in a navigationbaritem.
the code for userData is in the UserData.swift file:
import SwiftUI
final class UserData: ObservableObject {
#Published var showFavoritesOnly = false
#Published var landmarks = landmarkData
}
and
#EnvironmentObject var userData: UserData
is used in the landmarkdetail.swift file where the button action is placed
Related
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.
Need help with this please.
I have a view with 2 date variables and I want to show a modal which have the datepicker and let user pick different dates for these variables.
Currently I have two buttons that show the same sheet but pass different variable to the modal.
The problem the variable don’t update after dismissing the modal.
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State private var secOneDate = Date()
#State private var secTwoDate = Date()
#State private var isDatepickerPresented = false
var body: some View {
VStack {
HStack{
Button{
isDatepickerPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.indigo)
}
.sheet(isPresented: $isDatepickerPresented){
DatePickView(selectDate: $secOneDate)
}
Text("SecOneDate: \(secOneDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
HStack{
Button{
isDatepickerPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.mint)
}
.sheet(isPresented: $isDatepickerPresented)
{
DatePickView(selectDate: $secTwoDate)
}
Text("SecTwoDate: \(secTwoDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
}
}
}
import SwiftUI
struct DatePickView: View {
#Environment(\.dismiss) private var dismiss
#Binding var selectDate: Date
var body: some View {
VStack(alignment: .center, spacing: 20) {
HStack {
Text("\(selectDate)")
.padding()
Spacer()
Button {
dismiss()
} label: {
Image(systemName: "delete.backward.fill")
.foregroundColor(.indigo)
}
}.padding()
DatePicker("", selection: $selectDate)
.datePickerStyle(.graphical)
}
}
}
First of all, thank you for your minimal, reproducible example: it is clear and can be immediately used for debugging. Answering to your question:
The problem with your code is that you have only one variable that opens the sheet for both dates. Even though you are correctly passing the two different #Bindings, when you toggle isDatepickerPresented you are asking SwiftUI to show both sheets, but this will never happen. Without knowing, you are always triggering the first of the sheet presentations - the one that binds secOneDate. The sheet that binds secTwoDate is never shown because you can't have two sheets simultaneously.
With that understanding, the solution is simple: use two different trigger variables. Here's the code corrected (DatePickView doesn't change):
struct Example: View {
#State private var secOneDate = Date()
#State private var secTwoDate = Date()
#State private var isDatepickerOnePresented = false
#State private var isDatepickerTwoPresented = false
var body: some View {
VStack {
HStack{
Button{
isDatepickerOnePresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.indigo)
}
.sheet(isPresented: $isDatepickerOnePresented){
DatePickView(selectDate: $secOneDate)
}
Text("SecOneDate: \(secOneDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
HStack{
Button{
isDatepickerTwoPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.mint)
}
.sheet(isPresented: $isDatepickerTwoPresented) {
DatePickView(selectDate: $secTwoDate)
}
Text("SecTwoDate: \(secTwoDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
}
}
}
I have some problem with presenting sheet, sheet that use Identifiable item binding and fullScreenCover.
Main issue is that:
I choose identifiable item, corresponding sheet appears
I close that sheet with swipe
After it's dismiss i open normal sheet
It open again identifiable item sheet content
Another weird behaviour is if I open fullScreenCover after dismissing sheet it's open sheet but with content of fullScreenCover
This never happens on iOS 14. I think that is states that responsible for presenting those views not have time to update
You can checkout minimal gist reproduction or see it here
import SwiftUI
struct ContentView: View {
enum Sheet: String, Identifiable {
var id: String {
rawValue
}
case one, two
}
#State var isFullScreenPresented: Bool = false
#State var isSheetPresented: Bool = false
#State var isItemSheetPresented: Sheet?
var body: some View {
VStack {
HStack {
Button(action: {isFullScreenPresented.toggle()}, label: {
Text("fullScreen")
.padding()
.foregroundColor(.red)
.background(Rectangle())
})
Button(action: {isSheetPresented.toggle()}, label: {
Text("sheet")
.padding()
.foregroundColor(.red)
.background(Rectangle())
})
}
HStack {
Button(action: {isItemSheetPresented = .one}, label: {
Text("one")
.padding()
.foregroundColor(.red)
.background(Rectangle())
})
Button(action: {isItemSheetPresented = .two}, label: {
Text("two")
.padding()
.foregroundColor(.red)
.background(Rectangle())
})
}
HStack {
Text(isFullScreenPresented.description)
Text(isSheetPresented.description)
}
Text(isItemSheetPresented.debugDescription)
Spacer()
}
.sheet(item: $isItemSheetPresented, onDismiss: {isItemSheetPresented = nil}, content: {item in
Text(item.id)
})
.sheet(isPresented: $isSheetPresented, onDismiss: { isSheetPresented = false}, content: {
Text("sheet")
})
.fullScreenCover(isPresented: $isFullScreenPresented, onDismiss: {isFullScreenPresented = false }, content: {FullScreenContent()})
}
}
struct FullScreenContent: View {
#Environment(\.presentationMode) var dismiss
var body: some View {
VStack {
Button("close", action: {dismiss.wrappedValue.dismiss()})
Spacer()
}
}
}
I have two views, ViewAssignment and TaskDetailView. My ViewAssignment page fetches data from an environment object, and creates a list using the data.
Upon each item of the list being clicked on, the TaskDetailView pops in as a navigation link, however, I am having trouble making the information in the TaskDetailView unique to that particular iteration (the item in the list)
I believe the trouble comes from my TaskDetailView.swift
import SwiftUI
struct TaskDetailView: View {
#EnvironmentObject var assignment: Assignments
#State var taskNotes = ""
var body: some View {
VStack(spacing: 10) {
Image("english-essay")
.resizable()
.scaledToFit()
.frame(width: 250, height: 160)
.cornerRadius(20)
Text(self.assignment.data.first?.taskName ?? "Untitled Task")
.font(.title2)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.center)
HStack(spacing: 20) {
Label(self.assignment.data.first?.weighting ?? "0", systemImage: "percent")
.font(.subheadline)
.foregroundColor(.secondary)
Text(self.assignment.data.first?.dueDate ?? "No Date")
.font(.subheadline)
.foregroundColor(.secondary)
}
TextField("Write any notes here", text: $taskNotes)
.font(.body)
.padding()
Spacer()
}
}
}
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView() // I assume there is some information I have to pass through here
}
}
For details, this is my other view:
import SwiftUI
struct ViewAssignment: View {
// Observed to update the UI
#EnvironmentObject var assignment: Assignments
var body: some View {
ZStack {
NavigationView {
List(self.assignment.data) { task in
NavigationLink (
destination: TaskDetailView(),
label: {
Image(systemName: "doc.append.fill")
.scaleEffect(2.5)
.padding()
VStack(alignment: .leading, spacing: 3) {
Text(task.taskName)
.fontWeight(.semibold)
.lineLimit(2)
Text(task.dueDate + " - " + task.subject)
.font(.subheadline)
.foregroundColor(.secondary)
}
})
}
.navigationTitle("My Tasks")
.listStyle(InsetGroupedListStyle())
}
}
}
}
struct ViewAssignment_Previews: PreviewProvider {
static var previews: some View {
ViewAssignment()
}
}
I would also like to know if, upon making the screen unique for each item in the list, would I be able to have the contents of the text field saved upon reloading the app, Perhaps through #AppStorage?
Thank you for the assistance.
If I understand correctly what you are trying to do:
a TaskDetailView displays the detail of a ... Task.
So you should have a Task structure like this:
struct Task {
let name: String
let subject: String
...
}
You have to create one (or more) instance of Task to test your TaskDetailView:
extension Task {
var test: Task {
Task(name: "Test", subject: "Test Subject")
}
}
Now in the preview of your TaskDetailView you can try to display your example :
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView(task: Task.test) // here
}
}
For the moment nothing is happening. Because your TaskDetailView doesn't have a task parameter.
struct TaskDetailView: View {
var task: Task
var body: some View {
...
}
Now its body can use the different parameters of this Task.
Text(task.name)
.font(.title2)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.center)
Now in your List:
List(self.assignment.data) { task in
NavigationLink (
destination: TaskDetailView(task: task), // <- here !!!
label: {
Image(systemName: "doc.append.fill")
.scaleEffect(2.5)
.padding()
VStack(alignment: .leading, spacing: 3) {
Text(task.name)
.fontWeight(.semibold)
.lineLimit(2)
}
})
}
NavigationBarItem can't Click after dismiss view!
XCode11 beta3,
MacOS Catalina 10.15 Beta(19A501i)
When click DetailView button to dismiss by #Binding,
ContentView's navigationBarItem will disabled(Can't Click)!
But scroll down to dismiss will be fine(can click and will be print "Clicked!" in Debug Preview Mode)
struct DetailView: View {
#Binding var isPresented: Bool
var body: some View {
Group {
Text("Detail")
Button(action: {
self.isPresented.toggle()
}) {
Text("Dismiss")
}
}
}
}
struct ContentView : View {
#State var isPresented = false
var body: some View {
NavigationView{
Button(action: {self.isPresented.toggle()}){
Text("Show")
}
.presentation(!isPresented ? nil :
Modal(DetailView(isPresented: $isPresented)) {
print("dismissed")
}
)
.navigationBarTitle(Text("Test"))
.navigationBarItems(trailing:
Button(action: {print("Clicked!")} ) {
Image(systemName: "plus")
.frame(width: 44, height: 44)
.foregroundColor(.black)
.cornerRadius(22)
}
.padding(.trailing)
)
}
}
}
I'm inclined to think that there is a bug with modals. The onDismiss is never called when the modal goes away. However, I did found a workaround. Instead of dismissing by setting the isPresented variable from inside the modal view, I use the rootViewController from the main window, to call the UIKit dismiss method.
By dismissing the modal this way, the onDismiss closure is called properly, and it is there where I set isPresented = false, so the modal can be presented again.
The following code works, at least until a new version fixes the problem:
import SwiftUI
struct DetailView: View {
var body: some View {
Group {
Text("Detail")
Button(action: {
UIApplication.shared.windows[0].rootViewController?.dismiss(animated: true, completion: { })
}) {
Text("Dismiss")
}
}
}
}
struct ContentView : View {
#State var isPresented = false
var body: some View {
NavigationView{
Button(action: {self.isPresented.toggle()}){
Text("Show")
}
.presentation(!isPresented ? nil :
Modal(DetailView()) {
self.isPresented = false
print("dismissed")
}
)
.navigationBarTitle(Text("Test"))
.navigationBarItems(trailing:
Button(action: {print("Clicked!")} ) {
Image(systemName: "plus")
.frame(width: 44, height: 44)
.foregroundColor(.black)
.cornerRadius(22)
}
.padding(.trailing)
)
}
}
}