How do I initialize a Bool in this SwiftUI #ViewBuilder based on an optional Binding<Image>? - binding

My goal here is to use a ternary operator to add a shadow to my Image, but not add one if the View is using the default image.
This is my code:
import SwiftUI
struct ImageDisplay: View {
#State private var defaultImage: Image = Image(systemName: "person.crop.circle.fill")
private var defaultText: String = "Add picture"
private var hasShadow: Bool = true
private var text: String?
private var image: Binding<Image>?
public init(_ text: String, image: Binding<Image>) {
self.init(
text: .some(text),
image: image
)
}
public init(image: Binding<Image>) {
self.init(
text: nil,
image: image
)
}
public init() {
self.init(
text: nil,
image: nil
)
}
private init(text: String?, image: Binding<Image>?) {
self.text = text
self.image = image
}
private struct InternalImageDisplay: View {
var text: String
var hasShadow: Bool /// NOT CERTAIN WHERE THIS BELONGS, OR IF IT GOES HERE ...
#Binding var image: Image
#ViewBuilder
var body: some View {
VStack {
image
.imageDisplayStyle()
.shadow(radius: hasShadow ? 4.0 : 0.0) /// HERE IS WHERE I'LL USE IT...
Button(action: {
//TODO: - Code to present image picker.
}, label: {
Text(text)
})
}
}
}
var body: some View {
InternalImageDisplay(
text: text ?? defaultText,
hasShadow: false, /// THIS IS NOT WORKING IN ANY IMPLEMENTATION ...
image: image ?? $defaultImage
)
}
}
I also have this extension to Image:
import SwiftUI
extension Image {
func imageDisplayStyle() -> some View {
return self
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.foregroundColor(.gray)
}
}
I am attempting to get code that looks like this to work nicely and it's great , except for the shadow bool:
struct ContentView: View {
#State private var image = Image("img1")
#State private var defaultImage: Bool = true
var body: some View {
VStack (spacing: 48) {
// no binding
ImageDisplay()
// binding
ImageDisplay("Select image", image: $image)
ImageDisplay(image: $image)
}
.frame(width: 190)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

In your initializers, you can set hasShadow based on whether or not there's an image parameter being passed in.
Then, you can pass that through to your InternalImageDisplay.
I made the shadow a little more obvious just for testing purposes:
struct ImageDisplay: View {
#State private var defaultImage: Image = Image(systemName: "person.crop.circle.fill")
private var defaultText: String = "Add picture"
private var hasShadow: Bool = true
private var text: String?
private var image: Binding<Image>?
public init(_ text: String, image: Binding<Image>) {
self.init(
text: text,
image: image,
hasShadow: true //<-- Here
)
}
public init(image: Binding<Image>) {
self.init(
text: nil,
image: image,
hasShadow: true //<-- Here
)
}
public init() {
self.init(
text: nil,
image: nil,
hasShadow: false //<-- Here
)
}
private init(text: String?, image: Binding<Image>?, hasShadow : Bool) { //<-- Here
self.text = text
self.image = image
self.hasShadow = hasShadow //<-- Here
}
private struct InternalImageDisplay: View {
var text: String
var hasShadow: Bool //<-- This gets passed in as a prop
#Binding var image: Image
#ViewBuilder
var body: some View {
VStack {
image
.imageDisplayStyle()
.shadow(color: Color.green, radius: hasShadow ? 10.0 : 0) //<-- Here (now it's green for testing)
Button(action: {
//TODO: - Code to present image picker.
}, label: {
Text(text)
})
}
}
}
var body: some View {
InternalImageDisplay(
text: text ?? defaultText,
hasShadow: hasShadow, //<-- Here
image: image ?? $defaultImage
)
}
}

Related

ImagePicker showing duplicate photos

I am trying to understand why when I add a photo in my view it is duplicating the photo?
I want the user to add a photo to each card separately. I have tried to directly add selectedImage to my struct item image within the [Expense] array but xcode is screaming at me.
I know it has to with the #State selectedImage with its #Binding but Im not 100% sure how to enable this to work in my current scenario?
Here is the struct it is conforming to:
struct Card: Identifiable {
var id = UUID()
var title: String
var expenses: [Expense]
mutating func addExpenses() {
expenses.append(Expense(expensetype: "", amount: 0.0))
}
}
struct Expense: Identifiable {
var id = UUID()
var expensetype: String
var amount: Double = 0.0
var image: UIImage?
}
I am creating an array of cards on a button press in my contentview {
struct ContentView: View {
#State private var cards = [Card]()
#State private var sourceType: UIImagePickerController.SourceType = .photoLibrary
#State private var selectedImage: UIImage?
#State private var isImagePickerDisplay = false
#State private var showingOptions = false
var title = ""
var expensetype = ""
var amount: Double = 0.0
var image: UIImage?
var body: some View {
NavigationStack {
Form {
List {
Button("Add card") {
addCard()
}
ForEach($cards) { a in
Section {
TextField("Title", text: a.title)
Button("Add expense") {
a.wrappedValue.addExpenses()
}
ForEach(a.expenses) { b in
if a.expenses.isEmpty == false {
TextField("my expense", text: b.expensetype)
TextField("amount", value: b.amount, format: .number)
Button("Add image") {
withAnimation {
showingOptions = true
}
}
.confirmationDialog("", isPresented: $showingOptions, titleVisibility: .hidden) {
Button("Take photo") {
self.sourceType = .camera
self.isImagePickerDisplay.toggle()
}
Button("Choose photo") {
self.sourceType = .photoLibrary
self.isImagePickerDisplay.toggle()
}
}
if selectedImage != nil {
Image(uiImage: self.selectedImage!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 150)
} else {
Image(systemName: "snow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 150)
}
}
}
}
}
}
}
.sheet(isPresented: self.$isImagePickerDisplay) {
ImagePickerView(selectedImage: self.$selectedImage, sourceType: self.sourceType)
}
}
}
func addCard() {
cards.append(Card(title: title, expenses: []))
}
}
Here is my ImagePickerView
struct ImagePickerView: UIViewControllerRepresentable {
#Binding var selectedImage: UIImage?
#Environment(\.presentationMode) var isPresented
var sourceType: UIImagePickerController.SourceType
func makeUIViewController(context: Context) -> some UIViewController {
let imagePicker = UIImagePickerController()
imagePicker.sourceType = self.sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(picker: self)
}
}

value being constant in swiftUI

I am sorry im not sure that the title makes sense but if u read, im sure u will understand my problem. I have declared a variable #State var timetext: Int32 in the file CreatingWorkout, with a textfield TextField("5000, 100 etc", value: $timetext, formatter: NumberFormatter()) When i go to the createWorkoutView file and try to present it with the sheet, it wants me to give a value to timetext. However, when i provide a value with the textfield it stays constantly value given when calling with the sheet. I will attach a video here for you to see.
CreatingWorkout.swift :
struct CreatingWorkout: View {
#State var workoutTitle: String
#State var desc: String
#State var timetext: Int32
#State private var iconColor = Color.black
#State var displayWorkout: String = ""
#Environment(\.dismiss) var dismiss
#Environment(\.managedObjectContext) private var viewContext
private func saveWorkout() {
do {
let workout = Workout(context: viewContext)
workout.title = workoutTitle
workout.time = timetext
workout.icon = displayWorkout
workout.descriptionn = desc
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
CreateWorkout.swift :
import SwiftUI
struct CreateWorkoutView: View {
#State private var showingCreateWorkout = false
#State var timetext: Int32 = 0
var body: some View {
NavigationView {
VStack {
VStack(alignment: .center, spacing: 20) {
Text("Fitzy")
.font(.largeTitle)
.fontWeight(.bold)
Text("Create your first Workout")
.font(.title3)
.foregroundColor(.gray)
Button {
showingCreateWorkout.toggle()
} label: {
Image(systemName: "plus")
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(Color("AccentColor"))
.frame(width: 50, height: 50)
)
}.sheet(isPresented: $showingCreateWorkout) {
CreatingWorkout(workoutTitle: "", desc: "", timetext: timetext)
}
}
}.navigationTitle("Create Workout")
}
}
}
struct CreateWorkoutView_Previews: PreviewProvider {
static var previews: some View {
CreateWorkoutView()
}
}
https://drive.google.com/file/d/1qQDQmap5bMz9LxzibV98epHW5urqUtZ7/view?usp=sharing
as mentioned in the comments, you need a #State property to pass the value
that you type in your TextField to the sheet with CreatingWorkout. Try something like this:
struct CreatingWorkout: View {
#State var workoutTitle: String
#State var desc: String
#State var timetext: Int32
// ....
var body: some View {
Text("\(timetext)")
}
}
struct CreateWorkout: View {
#State var showingCreateWorkout = false
#State var timetext: Int32 = 0 // <-- here
var body: some View {
VStack {
TextField("type a number", value: $timetext, format: .number).border(.red) // <-- here
Button {
showingCreateWorkout.toggle()
} label: {
Image(systemName: "plus").font(.largeTitle).foregroundColor(.red).padding()
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(Color("AccentColor"))
.frame(width: 50, height: 50)
)
}.sheet(isPresented: $showingCreateWorkout) {
CreatingWorkout(workoutTitle: "", desc: "", timetext: timetext) // <-- here
}
}
}
}

SwiftUI: How to select multi items(image) with ForEach?

I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.

Trying to change a value inside a for in loop

I am trying to change a value inside a for in loop. The value Is a bool that I declared as a var and not a let but I get the error "Cannot use mutating member on immutable value: 'notat' is a 'let' constant"
So im trying to make it so that when I tap the image inside the button in my list it will change the completed (bool) value to true. And I want it so that when completed == true I get a filled checkmark
import SwiftUI
struct Notat : Identifiable
{
let id = UUID()
var cost: Int
var name: String
var completed: Bool
}
struct ContentView: View {
var modelData: [Notat] =
[Notat(cost: 50, name: "Klippe plenen", completed: false),
Notat(cost: 100, name: "Vaske speil", completed: true),
Notat(cost: 150, name: "Støvsuge huset", completed: false),
Notat(cost: 50, name: "Vaske bilen", completed: true)]
var body: some View {
List(modelData)
{
notat in HStack
{
Text("\(notat.cost)kr").frame(width: 50, height: 10, alignment: .leading)
Text(notat.name)
Button(action: /*#START_MENU_TOKEN#*/{}/*#END_MENU_TOKEN#*/)
{
Image(systemName: checkmarkSymbol(completed: notat.completed)).font(Font.system(size: 25, weight: .light))
.onTapGesture
{
test(notat: notat)
}
}.frame(maxWidth: .infinity, alignment: .trailing)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
func checkmarkSymbol(completed: Bool) -> String
{
if(completed)
{
return "checkmark.square.fill"
}else
{
return "checkmark.square"
}
}
func test(notat: Notat)
{
notat.completed.toggle() //Here is the error "Cannot use mutating member on immutable value: 'notat' is a 'let' constant"
}
You could do something like the following:
import SwiftUI
struct Notat : Identifiable, Equatable {
let id = UUID()
var cost: Int
var name: String
var completed: Bool
}
//You should change the name to something more descriptive than ViewModel...
class ViewModel: ObservableObject {
#Published var modelData = [
Notat(cost: 50, name: "Klippe plenen", completed: false),
Notat(cost: 100, name: "Vaske speil", completed: true),
Notat(cost: 150, name: "Støvsuge huset", completed: false),
Notat(cost: 50, name: "Vaske bilen", completed: true)
]
func setCompleted(for notat: Notat) {
guard let index = modelData.firstIndex(of: notat) else { return }
modelData[index].completed.toggle()
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
List(viewModel.modelData) { notat in HStack {
Text("\(notat.cost)kr").frame(width: 50, height: 10, alignment: .leading)
Text(notat.name)
Button(action: {})
{
Image(systemName: self.checkmarkSymbol(completed: notat.completed))
.font(Font.system(size: 25, weight: .light))
.onTapGesture
{
self.viewModel.setCompleted(for: notat)
}
}.frame(maxWidth: .infinity, alignment: .trailing)
}
}
}
func checkmarkSymbol(completed: Bool) -> String {
if (completed) {
return "checkmark.square.fill"
}
else {
return "checkmark.square"
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
So basically each time you are tapping on an image, the ViewModel will toggle the completed property and since modelData is marked with #Publish these changes will causing the View to reload itself.
Here it's descriped in more detail...

Navigation Bar Items with #EnvironmentObject

I would like to create a Settings view using SwiftUI. I mainly took the official example from Apple about SwiftUI to realize my code. The settings view should have a toggle to whether display or not my favorites items.
For now I have a landmarks list and a settings view.
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var imageName: String
var title: String
var isFavorite: Bool
var description: String
enum CodingKeys: String, CodingKey {
case id, imageName, title, description
}
}
final class UserData: ObservableObject {
#Published var showFavoriteOnly: Bool = false
#Published var items: [Landmark] = landmarkData
#Published var showProfile: Bool = false
}
struct ItemList: View {
#EnvironmentObject var userData: UserData
#State var trailing: Bool = false
init() {
UITableView.appearance().separatorStyle = .none
}
var body: some View {
NavigationView {
List {
VStack {
CircleBadgeView(text: String(landmarkData.count), thickness: 2)
Text("Tutorials available")
}.frame(minWidth:0, maxWidth: .infinity)
ForEach(userData.items) { landmark in
if !self.userData.showFavoriteOnly || landmark.isFavorite {
ZStack {
Image(landmark.imageName)
.resizable()
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.overlay(ImageOverlay(text: landmark.title), alignment: .bottomTrailing)
Text(String(landmark.isFavorite))
NavigationLink(destination: TutorialDetailView(landmark: landmark)) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
}
}
}.navigationBarTitle("Tutorials")
.navigationBarItems(trailing: trailingItem())
}
}
}
extension ItemList {
func trailingItem () -> some View {
return HStack {
if userData.showProfile {
NavigationLink(destination: ProfileView()) {
Image(systemName: "person.circle")
.imageScale(.large)
.accessibility(label: Text("Profile"))
}
}
NavigationLink(destination: SettingsView().environmentObject(userData)) {
Image(systemName: "gear")
.imageScale(.large)
.accessibility(label: Text("Settings"))
}
}
}
}
As you can see my SettingsView is accessible from navigationBarItems of my NavigationView. I don't know if it's the problem or not but when I put the Toggle inside the ListView it works as expected. But now when I trigger the toggle to enable only favorite my application crash instantly.
I've tried to trigger the Show profile toggle from SettingsView and it works.
struct SettingsView: View {
#EnvironmentObject var userData: UserData
var body: some View {
Form {
Section(header: Text("General")) {
Toggle(isOn: $userData.showProfile) {
Text("Show profile")
}
Toggle(isOn: $userData.showFavoriteOnly) {
Text("Favorites only")
}
}
Section(header: Text("UI")) {
Toggle(isOn: .constant(false)) {
Text("Dark mode")
}
NavigationLink(destination: Text("third")) {
Text("Third navigation")
}
}
}.navigationBarTitle(Text("Settings"), displayMode: .inline)
}
}
In brief, the crash appears in my SettingsView when I trigger the Show only favorite Toggle and then I try to go back to the previous view which is ItemListView
The only information I can get about the error is Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
You can find the whole project on my GitHub : https://github.com/Hurobaki/swiftui-tutorial
Some help would be really appreciated :)
Here is a minimal version of your example code, that works:
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var imageName: String
var title: String
var isFavorite: Bool
var description: String
}
final class UserData: ObservableObject {
#Published var showFavoriteOnly: Bool = false
#Published var items: [Landmark] = [
Landmark(id: 1, imageName: "a", title: "a", isFavorite: true, description: "A"),
Landmark(id: 2, imageName: "b", title: "b", isFavorite: false, description: "B")
]
}
struct ContentView: View {
#EnvironmentObject var userData: UserData
var body: some View {
NavigationView {
List(userData.items.filter { !userData.showFavoriteOnly || $0.isFavorite }) { landmark in
Text(String(landmark.isFavorite))
}
.navigationBarTitle("Tutorials")
.navigationBarItems(trailing: trailingItem())
}
}
func trailingItem () -> some View {
return HStack {
NavigationLink(destination: SettingsView()) {
Text("Settings")
}
}
}
}
struct SettingsView: View {
#EnvironmentObject var userData: UserData
var body: some View {
Form {
Section(header: Text("General")) {
Toggle(isOn: $userData.showFavoriteOnly) {
Text("Favorites only")
}
}
}.navigationBarTitle(Text("Settings"), displayMode: .inline)
}
}

Resources