I am new to SwiftUI and am facing an issue.
The variable selectedColor is not getting updated, when I move the slider.
My aim is as I move the slider the ColorView Background color should change and appropriate color should be shown as background but color is not getting changed. Any pointers to solve the problem would be helpful.
This is how it looks while I expect a capsule with selected color aroun #837c7c.
struct GradientView: View {
let name: String
#State private var redValue = 124.0
#State private var greenValue = 124.0
#State private var blueValue = 124.0
#State private var selectedColor: Color = .green
#State var isEditing = false
var body: some View {
VStack(spacing: 0) {
VStack {
HStack {
VStack {
Slider(value: $redValue,
in: 0...255,
step: 1
).onChange(of: self.redValue, perform: { newValue in
self.sliderChanged()
})
.accentColor(.red)
Slider(value: $greenValue,
in: 0...255,
step: 1
).onChange(of: self.greenValue, perform: { newValue in
self.sliderChanged()
})
.accentColor(.green)
Slider(value: $blueValue,
in: 0...255,
step: 1
).onChange(of: self.blueValue, perform: { newValue in
self.sliderChanged()
}).accentColor(.blue)
}
VStack {
ColorView(selectedColor: $selectedColor)
.padding()
//.foregroundColor(.black)
}
//.background(selectedColor)//.background(Capsule().fill(selectedColor))
//.init(red: redValue, green: greenValue, blue: blueValue)
}
}
}
.padding()
.frame(maxHeight: .infinity, alignment: .top)
.navigationTitle(name)
}
func sliderChanged() {
selectedColor = Color.init(red: redValue, green: greenValue, blue: blueValue)
print("Slider value changed to ")
print("\(redValue):\(greenValue):\(blueValue)")
}
}
struct ColorView: View {
#Binding var selectedColor: Color
var body: some View {
let components = selectedColor.cgColor?.components
Text("#\(String(format:"%02X", Int(components?[0] ?? 0)))\(String(format:"%02X", Int(components?[1] ?? 0)))\(String(format:"%02X", Int(components?[2] ?? 0)))")
.background(Capsule().fill(selectedColor))
.padding()
.font(.system(.caption).bold())
}
}
The Color constructor with components requires colour in 0...1, so we just need to convert slider value to needed range:
selectedColor = Color.init(red: CGFloat(redValue)/255,
green: CGFloat(greenValue/255),
blue: CGFloat(blueValue)/255)
Tested with Xcode 14 / iOS 16
Update: however instead I recommend to change selectedColor to CGColor type, because SwiftUI.Color.cgColor can return nil (as documented)
so more appropriate solution would be:
#State private var selectedColor: CGColor = UIColor.green.cgColor // << any default !!
// ...
func sliderChanged() {
selectedColor = CGColor.init(red: CGFloat(redValue)/255, green: CGFloat(greenValue/255), blue: CGFloat(blueValue)/255, alpha: 1.0)
print("Slider value changed to ")
print("\(redValue):\(greenValue):\(blueValue)")
}
}
struct ColorView: View {
#Binding var selectedColor: CGColor
var body: some View {
let components: [CGFloat] = selectedColor.components ?? [0, 0, 0]
Text("#\(String(format:"%02X", Int(components[0]*255)))\(String(format:"%02X", Int(components[1]*255)))\(String(format:"%02X", Int(components[2]*255)))")
.padding()
.background(Capsule().fill(Color(selectedColor)))
.font(.system(.caption).bold())
}
}
Related
i've created a KetupatButton class with 2 method like this:
class KetupatButton {
var condition = false
#State var colorToShow = Color(UIColor(red: 183/255, green: 191/255, blue: 150/255, alpha: 100))
func colorChange() {
if condition == false {
colorToShow = Color(UIColor(red: 183/255, green: 191/255, blue: 150/255, alpha: 100))
} else {
colorToShow = Color(UIColor(red: 19/255, green: 58/255, blue: 27/255, alpha: 100))
}
}
func createButton(size: CGFloat) -> some View {
return AnyView(Button(action: {
self.condition = !self.condition
self.colorChange()
print(self.colorToShow)
print(self.condition)
}, label: {
Rectangle()
.frame(width: size, height: size, alignment: .leading)
.foregroundColor(colorToShow)
}))
}
}
But, when I call that class from my ContentView and tap the button, the button don't change it's color. Even though when i print the colorToShow variable, it changed. But the UI color of the button didn't change...
Here is my ContentView
struct ContentView: View {
var button1 = KetupatButton()
var body: some View {
button1.createButton(size: 200)
}
}
You should follow a more structured approach: separate your view model, which is your class, from the views. Here are some steps to follow:
Your view model should not contain the view, if you can avoid it. Create a specific view to show the button, separated from the logic. Here's how your button could look like:
struct ButtonView: View {
// Receive the view model
#ObservedObject var viewModel: KetupatButton
let size: CGFloat
var body: some View {
Button {
// Use this to animate the change
withAnimation {
// The condition will automatically change the color, see the view model code
viewModel.condition.toggle()
}
} label: {
Rectangle()
.frame(width: size, height: size, alignment: .leading)
.foregroundColor(viewModel.colorToShow)
}
}
}
Your view model should be an Observable Object. Also, the values that trigger a change in the UI should be #Published variables. In addition, I also made in a way that the color changes automatically when the condition changes, so you can get rid of colorChnage().
class KetupatButton: ObservableObject {
// When this variable changes, it will change also the color.
var condition = false {
didSet {
if condition {
colorToShow = Color(UIColor(red: 19/255, green: 58/255, blue: 27/255, alpha: 100))
} else {
colorToShow = Color(UIColor(red: 183/255, green: 191/255, blue: 150/255, alpha: 100))
}
}
}
#Published var colorToShow = Color(UIColor(red: 183/255, green: 191/255, blue: 150/255, alpha: 100))
}
Finally, ContentView should create an instance of the view model as a #StateObject to share with subviews, like ButtonView that we created above:
struct ContentView: View {
#StateObject var viewModel = KetupatButton()
var body: some View {
ButtonView(viewModel: viewModel, size: 200)
}
}
Now, you can see that the color of the button changes.
So I am trying to make my TabView height dynamic. I have been looking for a way to do this but I can't seem to find a solution anywhere. This is how my code looks like.
struct ContentView: View {
#State var contentHeight: CGFloat = 0
var body: some View {
NavigationView {
ScrollView {
VStack {
TabView {
TestView1(contentHeight: $contentHeight)
TestView2(contentHeight: $contentHeight)
}
.tabViewStyle(.page)
.frame(height: contentHeight)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.background(.yellow)
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Test Project")
}
}
}
}
This is how my test1view and test2view look like.
struct TestView1: View {
#State var height: CGFloat = 0
#Binding var contentHeight: CGFloat
var body: some View {
Color.red
.frame(maxWidth:.infinity, minHeight: 200, maxHeight: 200)
.background(
GeometryReader { geo in
Color.clear
.preference(
key: HeightPreferenceKey.self,
value: geo.size.height
)
.onAppear {
contentHeight = height
}
}
.onPreferenceChange(HeightPreferenceKey.self) { height in
self.height = height
}
)
}
}
struct TestView2: View {
#Binding var contentHeight: CGFloat
#State var height: CGFloat = 0
var body: some View {
Color.black
.frame(maxWidth:.infinity, minHeight: 350, maxHeight: 350)
.background(
GeometryReader { geo in
Color.clear
.preference(
key: HeightPreferenceKey.self,
value: geo.size.height
)
.onAppear {
contentHeight = height
}
}
.onPreferenceChange(HeightPreferenceKey.self) { height in
self.height = height
}
)
}
}
struct HeightPreferenceKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
Now the problem is that when I drag it just a little the height changes. So when I drag it a little to the left the height changes to the height of TestView2 and it is still on TestView1.
I tried to add a drag gesture but it didn't let me swipe to the next page. So I don't know how I will be able to achieve this. Ive been looking for a solution but still no luck.
You can use the TabView($selection) initializer to do this. https://developer.apple.com/documentation/swiftui/tabview/init(selection:content:)
It tells you which tab you're currently viewing. Based off the middle point of the screen. And you don't have to deal with nasty GeometryReader and HeightPreferenceKey.
Here's your updated code. I even added a nice animation to fade between the two heights!
struct ContentView: View {
#State var selectedTab: Tab = .first
#State var animatedContentHeight: CGFloat = 300
enum Tab {
case first
case second
var contentHeight: CGFloat {
switch self {
case .first:
return 200
case .second:
return 350
}
}
}
var body: some View {
TabView(selection: $selectedTab) {
TestView1()
.tag(Tab.first)
TestView2()
.tag(Tab.second)
}
.tabViewStyle(.page)
// .frame(height: selectedTab.contentHeight) // Uncomment to see without animation
.frame(height: animatedContentHeight)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.onChange(of: selectedTab) { newValue in
print("now selected:", newValue)
withAnimation { animatedContentHeight = selectedTab.contentHeight }
}
}
}
struct TestView1: View {
var body: some View {
Color.red
}
}
struct TestView2: View {
var body: some View {
Color.black
}
}
The following code successfully shows a set of colors and displays the selected color when the colors are tapped, similar to the standard ColorPicker. What I would like to be able to do is use this custom color picker in other views, in a similar way as the standard ColorPicker. My issue is that I cannot expose the selected color to other views.
ContentView:
struct ContentView: View {
var body: some View {
VStack{
CustomColorPicker()
}
}
}
Custom Color Picker:
struct CustomColorPicker: View {
var colors: [UIColor] = [.red, .green, .blue, .purple, .orange]
#State var selectedColor = UIColor.systemGray6
var body: some View {
VStack {
Rectangle()
.fill(Color(selectedColor))
.frame(width: 45, height: 45)
HStack(spacing: 0) {
ForEach(colors, id: \.self) { color in
Button {
selectedColor = color
} label: {
Color(color)
.border(Color.gray, width: color == selectedColor ? 2 : 0)
}
}
}
.frame(height: 50.0)
}
}
}
I have tied using a model/ObservableObject to be able to capture the selected color in other views but it doesn't when you select the colors.
How can I make the Rectangle in ContentView update its fill color when a color in the color picker is tapped?
Or in general, what would be the best way to create a reusable custom color picker?
Using an ObservableObject
Content View
struct ContentView: View {
#ObservedObject var cModel = ColorPickerModel()
var body: some View {
VStack{
CustomColorPicker()
Rectangle()
.fill(cModel.selectedColor)
.frame(width: 100, height: 100)
}
}
}
Custom Color Picker
class ColorPickerModel:ObservableObject{
#Published var selectedColor:Color = Color.orange
}
struct CustomColorPicker: View {
var colors: [UIColor] = [.red, .green, .blue, .purple, .orange]
#StateObject var cModel = ColorPickerModel()
var body: some View {
VStack {
Rectangle()
.fill(cModel.selectedColor)
.frame(width: 45, height: 45)
HStack(spacing: 0) {
ForEach(colors, id: \.self) { color in
Button {
cModel.selectedColor = Color(color)
} label: {
Color(color)
//.border(Color.gray, width: color == selectedColor ? 2 : 0)
}
}
}
.frame(height: 50.0)
}
}
}
import SwiftUI
struct MyColorView: View {
//Source of truth
#StateObject var cModel = MyColorViewModel()
var body: some View {
VStack{
CustomColorPicker(selectedColor: $cModel.selectedColor)
Rectangle()
//Convert to the View Color
.fill(Color(cModel.selectedColor))
.frame(width: 100, height: 100)
}
}
}
//This will serve as the source of truth for this View and any View you share the ObservableObject with
//Share it using #ObservedObject and #EnvironmentObject
class MyColorViewModel:ObservableObject{
//Change to UIColor the types have to match
#Published var selectedColor: UIColor = .orange
}
struct CustomColorPicker: View {
var colors: [UIColor] = [.red, .green, .blue, .purple, .orange]
//You can now use this Picker with any Model
//Binding is a two-way connection it needs a source of truth
#Binding var selectedColor: UIColor
var body: some View {
VStack {
HStack(spacing: 0) {
ForEach(colors, id: \.self) { color in
Button {
selectedColor = color
} label: {
Color(color)
.border(Color.gray, width: color == selectedColor ? 2 : 0)
}
}
}
.frame(height: 50.0)
}
}
}
struct MyColorView_Previews: PreviewProvider {
static var previews: some View {
MyColorView()
}
}
Try the Apple SwiftUI Tutorials they are a good start.
I want to drag across rectangles in a grid and change their color. This code is almost working, but this is not always the right rectangle that reacts: it behaves rather randomly. Any hints?
import SwiftUI
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color] = Array(repeating: Color.gray, count: 4)
#State private var rect = [CGRect]()
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect.insert(geo.frame(in: .global), at: rect.endIndex)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}
If I correctly understood your goal, here is fixed variant (with small modifications to avoid repeated hardcoding).
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color]
#State private var rect: [CGRect]
init() {
_colors = State(initialValue: Array(repeating: .gray, count: data.count))
_rect = State(initialValue: Array(repeating: .zero, count: data.count))
}
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect[item] = geo.frame(in: .global)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}
I'm trying to make a graph app, but animating it using a #State property does not help, for some reason.
struct GraphBars: View {
#State var percent: CGFloat
var body: some View {
Capsule()
.fill(Color.black)
.frame(width: 50, height: self.percent)
}
}
struct TEST3: View {
#State var bar1: CGFloat = 90.0
var body: some View {
GeometryReader { gg in
VStack {
Button(action: {
self.bar1 = 300.0
}) {
Text("Hello")
}
GraphBars(percent: bar1)
}
However, pressing the button does not increase the height of the bar as I thought it would. What am I doing wrong?
You need to use #Binding to transfer a variable back and forth between two Views otherwise the parent View doesn't get a notice of the variable change.
Do it like this:
struct GraphBars: View {
#Binding var percent: CGFloat
var body: some View {
Capsule()
.fill(Color.black)
.frame(width: 50, height: self.percent)
}
}
struct Test3: View {
#State var bar1: CGFloat = 90.0
var body: some View {
GeometryReader { gg in
VStack {
Button(action: {
self.bar1 = 300.0
}) {
Text("Hello")
}
GraphBars(percent: self.$bar1)
}
}
}
}