SwiftUI Segmented Control selected segment text animation on view refresh - swiftui

I am experiencing the following animation of the text in the selected segment of Segmented Controls when the View is refreshed after changing some other data in the View:
Is this a bug/feature or is there a way to eliminate this behaviour?
This is the code to reproduce the effect:
import SwiftUI
struct ContentView: View {
let colorNames1 = ["Red", "Green", "Blue"]
#State private var color1 = 0
let colorNames2 = ["Yellow", "Purple", "Orange"]
#State private var color2 = 0
var body: some View {
VStack {
VStack {
Picker(selection: $color1, label: Text("Color")) {
ForEach(0..<3, id: \.self) { index in
Text(self.colorNames1[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Color 1: \(color1)")
}
.padding()
VStack {
Picker(selection: $color2, label: Text("Color")) {
ForEach(0..<3, id: \.self) { index in
Text(self.colorNames2[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Color 2: \(color2)")
}
.padding()
}
}
}
This was run under iOS 13.4 / Xcode 11.4

rearrange you code base ... (this helps SwiftUI to "refresh" only necessary Views)
import SwiftUI
struct ContentView: View {
let colorNames1 = ["Red", "Green", "Blue"]
#State private var color1 = 0
let colorNames2 = ["Yellow", "Purple", "Orange"]
#State private var color2 = 0
var body: some View {
VStack {
MyPicker(colorNames: colorNames1, color: $color1)
.padding()
MyPicker(colorNames: colorNames2, color: $color2)
.padding()
}
}
}
struct MyPicker: View {
let colorNames: [String]
#Binding var color: Int
var body: some View {
VStack {
Picker(selection: $color, label: Text("Color")) {
ForEach(0..<colorNames.count) { index in
Text(self.colorNames[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Color 1: \(color)")
}
}
}
struct ContetView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
result

I created a custom SegmentControl to solve this problem:
import SwiftUI
struct MyTextPreferenceKey: PreferenceKey {
typealias Value = [MyTextPreferenceData]
static var defaultValue: [MyTextPreferenceData] = []
static func reduce(value: inout [MyTextPreferenceData], nextValue: () -> [MyTextPreferenceData]) {
value.append(contentsOf: nextValue())
}
}
struct MyTextPreferenceData: Equatable {
let viewIndex: Int
let rect: CGRect
}
struct SegmentedControl : View {
#Binding var selectedIndex: Int
#Binding var rects: [CGRect]
#Binding var titles: [String]
var body: some View {
ZStack(alignment: .topLeading) {
SelectedView()
.frame(width: rects[selectedIndex].size.width - 4, height: rects[selectedIndex].size.height - 4)
.offset(x: rects[selectedIndex].minX + 2, y: rects[selectedIndex].minY + 2)
.animation(.easeInOut(duration: 0.5))
VStack {
self.addTitles()
}.onPreferenceChange(MyTextPreferenceKey.self) { preferences in
for p in preferences {
self.rects[p.viewIndex] = p.rect
}
}
}.background(Color(.red)).clipShape(Capsule()).coordinateSpace(name: "CustomSegmentedControl")
}
func totalSize() -> CGSize {
var totalSize: CGSize = .zero
for rect in rects {
totalSize.width += rect.width
totalSize.height = rect.height
}
return totalSize
}
func addTitles() -> some View {
HStack(alignment: .center, spacing: 8, content: {
ForEach(0..<titles.count) { index in
return SegmentView(selectedIndex: self.$selectedIndex, label: self.titles[index], index: index, isSelected: self.segmentIsSelected(selectedIndex: self.selectedIndex, segmentIndex: index))
}
})
}
func segmentIsSelected(selectedIndex: Int, segmentIndex: Int) -> Binding<Bool> {
return Binding(get: {
return selectedIndex == segmentIndex
}) { (value) in }
}
}
struct SegmentView: View {
#Binding var selectedIndex: Int
let label: String
let index: Int
#Binding var isSelected: Bool
var body: some View {
Text(label)
.padding(.vertical, 6)
.padding(.horizontal, 10)
.foregroundColor(Color(.label))
.background(MyPreferenceViewSetter(index: index)).onTapGesture {
self.selectedIndex = self.index
}
}
}
struct MyPreferenceViewSetter: View {
let index: Int
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: MyTextPreferenceKey.self,
value: [MyTextPreferenceData(viewIndex: self.index, rect: geometry.frame(in: .named("CustomSegmentedControl")))])
}
}
}
struct SelectedView: View {
var body: some View {
Capsule()
.fill(Color(.systemBackground))
.edgesIgnoringSafeArea(.horizontal)
}
}
result

Related

SwiftUI Preference Key to make uniformly sized boxes

UPDATE: I've made this minimally reproducible.
I wish to make a grid for the alphabet, with each box the same size, looking like this:
I have a PreferenceKey, and a View extension, like this:
struct WidthPreference: PreferenceKey {
static let defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
value = value ?? nextValue()
}
}
extension View {
func sizePreference(letterIdx: Int) -> some View {
background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreference.self, value: proxy.size.width)
})
}
}
My primary view is an HStack nestled in a VStack, with each letter as a separate view. Here is the ContentView, and its Alphabet Grid:
struct ContentView: View {
#StateObject var theModel = MyModel()
var body: some View {
AlphabetGrid()
.textCase(.uppercase)
.font(.body)
.onAppear() {
theModel.initializeLetters()
}
.environmentObject(theModel)
}
}
struct AlphabetGrid: View {
#EnvironmentObject var theModel: MyModel
var spacing: CGFloat = 8
var body: some View {
let theKeyboard = [ theModel.allLetters?.filter { $0.keyboardRow == 0 },
theModel.allLetters?.filter { $0.keyboardRow == 1 },
theModel.allLetters?.filter { $0.keyboardRow == 2 }
]
VStack {
ForEach(theKeyboard, id: \.self) { keyboardRow in
HStack(alignment: .top) {
if let keyboardRow = keyboardRow {
ForEach(keyboardRow, id: \.self) { keyboardLetter in
let idx = keyboardLetter.letterStorePosition
LetterView(theIdx: idx, borderColour: .blue)
}
}
}
}
}
}
}
And then the Letter view, for each letter:
struct LetterView: View {
#EnvironmentObject var theModel: MyModel
var theIdx: Int
var borderColour: Color
var spacing: CGFloat = 8
#State private var cellWidth: CGFloat? = nil
func letterFor(letterIdx: Int) -> some View {
Text(String(theModel.allLetters?[letterIdx].letterStoreChar ?? "*"))
.sizePreference(letterIdx: letterIdx)
.frame(width: cellWidth, height: cellWidth, alignment: .center)
.padding(spacing)
}
var body: some View {
self.letterFor(letterIdx: theIdx)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(borderColour, lineWidth: 1)
)
.onPreferenceChange(WidthPreference.self) { self.cellWidth = $0 }
}
}
Finally, for completeness, the Model to store the letters:
class MyModel: ObservableObject {
#Published var allLetters: [LetterData]?
struct LetterData: Hashable {
let letterStorePosition: Int
let letterStoreChar: Character
let keyboardRow: Int
let keyboardCol: Int
}
let keyboardWide = 9 // characters per row
// put all the alphabet characters into an array of LetterData elements
func initializeLetters() {
var tempLetters: [LetterData] = []
let allChars = Array("abcdefghijklmnopqrstuvwxyz")
for (index, element) in allChars.enumerated() {
let row = index / keyboardWide
let col = index % keyboardWide
tempLetters.append(LetterData(letterStorePosition: index, letterStoreChar: element,
keyboardRow: row, keyboardCol: col))
}
allLetters = tempLetters
}
}
Unfortunately, this makes a pretty, yet incorrect grid like this:
Any ideas on where I'm going wrong?
I did some digging, your PreferenceKey is being set with .background which just takes the size of the current View and you are using that value to turn into a square.
There is no match for the average just taking the current width and using it for the height.
extension View {
func sizePreference(letterIdx: Int) -> some View {
background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreference.self, value: proxy.size.width)
})
}
}
.frame(width: cellWidth, height: cellWidth, alignment: .center)
The width is based on the letter I being the most narrow and W being the widest.
Now, how to "fix" your code. You can move the onPreferenceChange up one View and use the min between the current cellWidth and the $0 instead of just replacing.
struct AlphabetGrid: View {
#EnvironmentObject var theModel: MyModel
#State private var cellWidth: CGFloat = .infinity
var spacing: CGFloat = 8
var body: some View {
let theKeyboard = [ theModel.allLetters?.filter { $0.keyboardRow == 0 },
theModel.allLetters?.filter { $0.keyboardRow == 1 },
theModel.allLetters?.filter { $0.keyboardRow == 2 }
]
VStack {
ForEach(theKeyboard, id: \.self) { keyboardRow in
HStack(alignment: .top) {
if let keyboardRow = keyboardRow {
ForEach(keyboardRow, id: \.self) { keyboardLetter in
let idx = keyboardLetter.letterStorePosition
LetterView(theIdx: idx, borderColour: .blue, cellWidth: $cellWidth)
}
}
}
}
} .onPreferenceChange(WidthPreference.self) { self.cellWidth = min(cellWidth, $0 ?? .infinity) }
}
}
Now with that fix you get a better looking keyboard but the M and W are cut off, to use the max you need a little more tweaking, ou can look at the code below.
import SwiftUI
class MyModel: ObservableObject {
#Published var allLetters: [LetterData]?
struct LetterData: Hashable {
let letterStorePosition: Int
let letterStoreChar: Character
let keyboardRow: Int
let keyboardCol: Int
}
let keyboardWide = 9 // characters per row
// put all the alphabet characters into an array of LetterData elements
func initializeLetters() {
var tempLetters: [LetterData] = []
let allChars = Array("abcdefghijklmnopqrstuvwxyz")
for (index, element) in allChars.enumerated() {
let row = index / keyboardWide
let col = index % keyboardWide
tempLetters.append(LetterData(letterStorePosition: index, letterStoreChar: element,
keyboardRow: row, keyboardCol: col))
}
allLetters = tempLetters
}
}
struct AlphabetParentView: View {
#StateObject var theModel = MyModel()
var body: some View {
AlphabetGrid()
.textCase(.uppercase)
.font(.body)
.onAppear() {
theModel.initializeLetters()
}
.environmentObject(theModel)
}
}
struct LetterView: View {
#EnvironmentObject var theModel: MyModel
var theIdx: Int
var borderColour: Color
var spacing: CGFloat = 8
#Binding var cellWidth: CGFloat?
func letterFor(letterIdx: Int) -> some View {
Text(String(theModel.allLetters?[letterIdx].letterStoreChar ?? "*"))
.padding(spacing)
}
var body: some View {
RoundedRectangle(cornerRadius: 8)
.stroke(borderColour, lineWidth: 1)
.overlay {
self.letterFor(letterIdx: theIdx)
}
.frame(width: cellWidth, height: cellWidth, alignment: .center)
}
}
struct AlphabetGrid: View {
#EnvironmentObject var theModel: MyModel
#State private var cellWidth: CGFloat? = nil
var spacing: CGFloat = 8
var body: some View {
let theKeyboard = [ theModel.allLetters?.filter { $0.keyboardRow == 0 },
theModel.allLetters?.filter { $0.keyboardRow == 1 },
theModel.allLetters?.filter { $0.keyboardRow == 2 }
]
VStack {
ForEach(theKeyboard, id: \.self) { keyboardRow in
HStack(alignment: .top) {
if let keyboardRow = keyboardRow {
ForEach(keyboardRow, id: \.self) { keyboardLetter in
let idx = keyboardLetter.letterStorePosition
LetterView(theIdx: idx, borderColour: .blue, cellWidth: $cellWidth)
.sizePreference()
}
}
}
}
} .onPreferenceChange(WidthPreference.self) {
if let w = cellWidth{
self.cellWidth = min(w, $0 ?? .infinity)
}else{
self.cellWidth = $0
}
}
}
}
struct AlphabetParentView_Previews: PreviewProvider {
static var previews: some View {
AlphabetParentView()
}
}
struct WidthPreference: PreferenceKey {
static let defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
value = value ?? nextValue()
}
}
extension View {
func sizePreference() -> some View {
background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreference.self, value: proxy.size.width)
})
}
}
There are simpler way of handling this like Ashley's example or SwiftUI.Layout and layout but this should help you understand why your squares were uneven.
Here's a fairly simple implementation, using a GeometryReader to allow us to calculate the width (and therefore the height), of each letter
struct ContentView: View {
let letters = ["ABCDEFGHI","JKLMNOPQR","STUVWXYZ"]
let spacing: CGFloat = 8
var body: some View {
GeometryReader { proxy in
VStack(spacing: spacing) {
ForEach(letters, id: \.self) { row in
HStack(spacing: spacing) {
ForEach(Array(row), id: \.self) { letter in
Text(String(letter))
.frame(width: letterWidth(for: proxy.size.width), height: letterWidth(for: proxy.size.width))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(.cyan, lineWidth: 1)
)
}
}
}
}
}
.padding()
}
func letterWidth(for width: CGFloat) -> CGFloat {
let count = CGFloat(letters.map(\.count).max()!)
return (width - (spacing * (count - 1))) / count
}
}

Creating a Vertical Stack of HStacks with text and sliders swiftUI

Hi I have a Stack of Hstacks that consist of a Text and a slider. The slider width extends to the edge of the text in front of it but I want them all to have the same width and appear in a straight column.
Like this below.
This is how I am forming the stacks.
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray)
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
this is the view where I am forming the sliders. Can someone pls help
struct MyNodeView : View {
#Binding var myNode : Sliders
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.percent))%").font(.footnote)
Slider(value: $myNode.percent, in: 0 ... 100)
}
}
}
To make sure all the text is the same width, the simplest way is .frame(width:).
struct Sliders {
var name = "Name"
var percent = CGFloat(100)
}
struct ContentView: View {
#State var defensiveLayers = [
Sliders(name: "Long name", percent: 80),
Sliders(name: "Name", percent: 70),
Sliders(name: "Ok name", percent: 65),
Sliders(name: "Hi", percent: 15),
Sliders(name: "Hello", percent: 45),
]
var body: some View {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray)
.frame(width: 100) /// add frame here!
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
}
}
struct MyNodeView : View {
#Binding var myNode: Sliders
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.percent))%").font(.footnote)
Slider(value: $myNode.percent, in: 0 ... 100)
}
}
}
Result:
You could calculate the required width for the longest Text and apply that particular width to all the leading Text views in all the HStacks unifying them in the width. I've created a helper method for finding the required width for any SwiftUI View.
Helper:
extension View {
func viewSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { }
}
Usage:
#State private var textFrameWidth: CGFloat = 0
var body: some View {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name)
.font(.custom("Gill Sans", size: 12))
.padding(.trailing)
.foregroundColor(.gray)
.viewSize { size in
textFrameWidth = textFrameWidth < size.width ? size.width : textFrameWidth
}
.frame(width: textFrameWidth)
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
}

How to Add Color beside text into the list in swiftUI?(Data Flow)

I try to get( text & color ) from user and add them to the list in SwiftUI I already can pass text data but unfortunately for color I can't while they should be the same, below there is an image of app.To work we should provide a Binding for PreAddTextField .Thanks for your help
here is my Code :
import SwiftUI
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "", color: .purple)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id:
\.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id), colorPickerColor: <#Binding<Color>#>)
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AddListView(showAddListView: .constant(false),appState: AppState())
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String
#Binding var colorPickerColor : Color
var body: some View {
HStack {
TextField("Enter text", text: $textInTextField)
ColorPicker("", selection: $colorPickerColor)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
// What should happen here to add Text to List???
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
DataModel :
import SwiftUI
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
var color : Color
}
class AppState : ObservableObject {
#Published var textData : [Text1] = [.init(text: "Item 1", color: .purple),.init(text: "Item 2", color: .purple)]
}
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "", color: .purple)] //start with one empty item
//save all of the new items -- don't save anything that is empty
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
//these Bindings get used for the TextFields -- they're attached to the item IDs
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.text ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, text: newValue, color: .purple)
}
}
}
}
and finaly :
import SwiftUI
struct ListView: View {
#StateObject var appState = AppState() //store the AppState here
#State private var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(appState.textData, id : \.self){ text in
HStack {
Image(systemName: "square")
.foregroundColor(text.color)
Text(text.text)
}
}
if showAddListView {
AddListView(showAddListView: $showAddListView, appState: appState)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Instead of using a Binding for just the String, you could create a Binding for the entire Text1 item:
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "", color: .purple)]
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
func bindingForId(id: UUID) -> Binding<Text1> { //now returns a Text1
.init { () -> Text1 in
self.textItemsToAdd.first(where: { $0.id == id }) ?? Text1(text: "", color: .clear)
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return newValue
}
}
}
}
struct PreAddTextField: View {
#Binding var item : Text1
var body: some View {
HStack {
TextField("Enter text", text: $item.text) //gets the text property of the binding
ColorPicker("", selection: $item.color) //gets the color property
}
}
}
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "", color: .purple)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in
PreAddTextField(item: viewModel.bindingForId(id: item.id)) //parameter is changed here
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}

How to Add multi text into the list in SwiftUI?(Data Flow)

I'm trying to build an demo app by swiftUI that get multi text from user and add them to the list, below , there is an image of app every time user press plus button the AddListView show to the user and there user can add multi text to the List.I have a problem to add them to the list by new switUI data Flow I don't know how to pass data.(I comment more information)
Thanks 🙏
here is my code for AddListView:
import SwiftUI
struct AddListView: View {
#State var numberOfTextFiled = 1
#Binding var showAddListView : Bool
var body: some View {
ZStack {
Title(numberOfTextFiled: $numberOfTextFiled)
VStack {
ScrollView {
ForEach(0 ..< numberOfTextFiled, id: \.self) { item in
PreAddTextField()
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView)
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AddListView(showAddListView: .constant(false))
}
}
struct PreAddTextField: View {
// I made this standalone struct and use #State to every TextField text be independent
// if i use #Binding to pass data all Texfield have the same text value
#State var textInTextField = ""
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
// What should happen here to add Text to List???
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
#Binding var numberOfTextFiled : Int
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
numberOfTextFiled += 1
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
and for DataModel:
import SwiftUI
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
var textData = [
Text1(text: "SwiftUI"),
Text1(text: "Data flow?"),
]
and finally:
import SwiftUI
struct ListView: View {
#State var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Because of the multiple-items part of the question, this becomes a lot less trivial. However, using a combination of ObservableObjects and callback functions, definitely doable. Look at the inline comments in the code for explanations about what is going on:
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
//Store the items in an ObservableObject instead of just in #State
class AppState : ObservableObject {
#Published var textData : [Text1] = [.init(text: "Item 1"),.init(text: "Item 2")]
}
//This view model stores data about all of the new items that are going to be added
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "")] //start with one empty item
//save all of the new items -- don't save anything that is empty
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
//these Bindings get used for the TextFields -- they're attached to the item IDs
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.text ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, text: newValue)
}
}
}
}
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "")) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String //this takes a binding to the view model now
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void //callback function for what happens when "Add" gets pressed
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void //callback function for what happens when the plus button is hit
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
struct ListView: View {
#StateObject var appState = AppState() //store the AppState here
#State private var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(appState.textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView, appState: appState)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}

SwiftUI doesn't update state to #ObservedObject cameraViewModel object

I'm new to SwiftUI and manual camera functionality, and I really need help.
So I trying to build a SwiftUI camera view that has a UIKit camera as a wrapper to control the focus lens position via SwiftUI picker view, display below a fucus value, and want to try have a correlation between AVcaptureDevice.lensPosition from 0 to 1.0 and feats that are displayed in the focus picker view. But for now, I only want to display that fucus number on screen.
And the problem is when I try to update focus via coordinator focus observation and set it to the camera view model then nothing happened. Please help 🙌
Here's the code:
import SwiftUI
import AVFoundation
import Combine
struct ContentView: View {
#State private var didTapCapture = false
#State private var focusLensPosition: Float = 0
#ObservedObject var cameraViewModel = CameraViewModel(focusLensPosition: 0)
var body: some View {
VStack {
ZStack {
CameraPreviewRepresentable(didTapCapture: $didTapCapture, cameraViewModel: cameraViewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
VStack {
FocusPicker(selectedFocus: $focusLensPosition)
Text(String(cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.edgesIgnoringSafeArea(.all)
Spacer()
CaptureButton(didTapCapture: $didTapCapture)
.frame(width: 100, height: 100, alignment: .center)
.padding(.bottom, 20)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CaptureButton: View {
#Binding var didTapCapture : Bool
var body: some View {
Button {
didTapCapture.toggle()
} label: {
Image(systemName: "photo")
.font(.largeTitle)
.padding(30)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.red)
)
}
}
}
struct CameraPreviewRepresentable: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var didTapCapture: Bool
#ObservedObject var cameraViewModel: CameraViewModel
let cameraController: CustomCameraController = CustomCameraController()
func makeUIViewController(context: Context) -> CustomCameraController {
cameraController.delegate = context.coordinator
return cameraController
}
func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
if (self.didTapCapture) {
cameraViewController.didTapRecord()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self, cameraViewModel: cameraViewModel)
}
class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
let parent: CameraPreviewRepresentable
var cameraViewModel: CameraViewModel
var focusLensPositionObserver: NSKeyValueObservation?
init(_ parent: CameraPreviewRepresentable, cameraViewModel: CameraViewModel) {
self.parent = parent
self.cameraViewModel = cameraViewModel
super.init()
focusLensPositionObserver = self.parent.cameraController.currentCamera?.observe(\.lensPosition, options: [.new]) { [weak self] camera, _ in
print(Float(camera.lensPosition))
//announcing changes via Publisher
self?.cameraViewModel.focusLensPosition = camera.lensPosition
}
}
deinit {
focusLensPositionObserver = nil
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
parent.didTapCapture = false
if let imageData = photo.fileDataRepresentation(), let image = UIImage(data: imageData) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
class CameraViewModel: ObservableObject {
#Published var focusLensPosition: Float = 0
init(focusLensPosition: Float) {
self.focusLensPosition = focusLensPosition
}
}
class CustomCameraController: UIViewController {
var image: UIImage?
var captureSession = AVCaptureSession()
var backCamera: AVCaptureDevice?
var frontCamera: AVCaptureDevice?
var currentCamera: AVCaptureDevice?
var photoOutput: AVCapturePhotoOutput?
var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
//DELEGATE
var delegate: AVCapturePhotoCaptureDelegate?
func showFocusLensPosition() -> Float {
// guard let camera = currentCamera else { return 0 }
// try! currentCamera!.lockForConfiguration()
// currentCamera!.focusMode = .autoFocus
//// currentCamera!.setFocusModeLocked(lensPosition: currentCamera!.lensPosition, completionHandler: nil)
// currentCamera!.unlockForConfiguration()
return currentCamera!.lensPosition
}
func didTapRecord() {
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: delegate!)
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
setupCaptureSession()
setupDevice()
setupInputOutput()
setupPreviewLayer()
startRunningCaptureSession()
}
func setupCaptureSession() {
captureSession.sessionPreset = .photo
}
func setupDevice() {
let deviceDiscoverySession =
AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .unspecified)
for device in deviceDiscoverySession.devices {
switch device.position {
case .front:
self.frontCamera = device
case .back:
self.backCamera = device
default:
break
}
}
self.currentCamera = self.backCamera
}
func setupInputOutput() {
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: currentCamera!)
captureSession.addInput(captureDeviceInput)
photoOutput = AVCapturePhotoOutput()
captureSession.addOutput(photoOutput!)
} catch {
print(error)
}
}
func setupPreviewLayer() {
self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
let deviceOrientation = UIDevice.current.orientation
cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue)!
self.cameraPreviewLayer?.frame = self.view.frame
// view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
}
func startRunningCaptureSession() {
captureSession.startRunning()
}
}
struct FocusPicker: View {
var feets = ["∞ ft", "30", "15", "10", "7", "5", "4", "3.5", "3", "2.5", "2", "1.5", "1", "0.5", "Auto"]
#Binding var selectedFocus: Float
var body: some View {
Picker(selection: $selectedFocus, label: Text("")) {
ForEach(0 ..< feets.count) {
Text(feets[$0])
.foregroundColor(.white)
.font(.subheadline)
.fontWeight(.medium)
}
.animation(.none)
.background(Color.clear)
.pickerStyle(WheelPickerStyle())
}
.frame(width: 60, height: 200)
.border(Color.gray, width: 5)
.clipped()
}
}
The problem with your provided code is that the type of selectedFocus within the FocusPicker view should be Integer rather than Float. So one option is to change this type to Integer and find a way to express the AVCaptureDevice.lensPosition as an Integer with the given range.
The second option is to replace the feets array with an enumeration. By making the enumeration conform to the CustomStringConvertible protocol, you can even provide a proper description. Please see my example below.
I've stripped your code a bit as you just wanted to display the number in the first step and thus the code is more comprehensible.
My working example:
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var cameraViewModel = CameraViewModel(focusLensPosition: 0.5)
var body: some View {
VStack {
ZStack {
VStack {
FocusPicker(selectedFocus: $cameraViewModel.focusLensPosition)
Text(String(self.cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.edgesIgnoringSafeArea(.all)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class CameraViewModel: ObservableObject {
#Published var focusLensPosition: Float
init(focusLensPosition: Float) {
self.focusLensPosition = focusLensPosition
}
}
enum Feets: Float, CustomStringConvertible, CaseIterable, Identifiable {
case case1 = 0.0
case case2 = 0.5
case case3 = 1.0
var id: Float { self.rawValue }
var description: String {
get {
switch self {
case .case1:
return "∞ ft"
case .case2:
return "4"
case .case3:
return "Auto"
}
}
}
}
struct FocusPicker: View {
#Binding var selectedFocus: Float
var body: some View {
Picker(selection: $selectedFocus, label: Text("")) {
ForEach(Feets.allCases) { feet in
Text(feet.description)
}
.animation(.none)
.background(Color.clear)
.pickerStyle(WheelPickerStyle())
}
.frame(width: 60, height: 200)
.border(Color.gray, width: 5)
.clipped()
}
}