Placing SwiftUI Data Sources Somewhere Else - swift

I'm trying to use SwiftUI in a project but beyond the very basic version of using #States and #Bindings that can be found in every tutorial, so I need some help on what I'm doing wrong here.
Environment Setup:
I have following files involved with this problem:
CustomTextField: It's a SwiftUI View that contains an internal TextField along with bunch of other things (According to the design)
CustomTextFieldConfiguration: Contains the things that I need to configure on my custom textfield view
RootView: It's a SwiftUI View that is using CustomTextField as one of it's subviews
RootPresenter: This is where the UI Logic & Presentation Logic goes (Between the view and business logic)
RootPresentationModel: It's the viewModel through which the Presenter can modify view's state
RootBuilder: It contains the builder class that uses the builder pattern to wire components together
The Problem:
The textField value does not update in the textValue property of rootPresentationModel
Here are the implementations (Partially) as I have done and have no idea where I have gone wrong:
CustomTextField:
struct CustomTextField: View {
#Binding var config: CustomTextFieldConfiguration
var body: some View {
ZStack {
VStack {
VStack {
ZStack {
HStack {
TextField($config.placeHolder,
value: $config.textValue,
formatter: NumberFormatter(),
onEditingChanged: {_ in },
onCommit: {})
.frame(height: 52.0)
.padding(EdgeInsets(top: 0, leading: 16 + ($config.detailActionImage != nil ? 44 : 0),
bottom: 0, trailing: 16 + ($config.contentAlignment == .center && $config.detailActionImage != nil ? 44 : 0)))
.background($config.backgroundColor)
.cornerRadius($config.cornerRedius)
.font($config.font)
...
...
...
...
CustomTextFieldConfiguration:
struct CustomTextFieldConfiguration {
#Binding var textValue: String
...
...
...
...
RootView:
struct RootView: View {
#State var configuration: CustomTextFieldConfiguration
var interactor: RootInteractorProtocol!
#Environment(\.colorScheme) private var colorScheme
var body: some View {
HStack {
Spacer(minLength: 40)
VStack(alignment: .trailing) {
CustomTextField(config: $configuration)
Text("\(configuration.textValue)")
}
Spacer(minLength: 40)
}
}
}
RootPresenter:
class RootPresenter: BasePresenter {
#ObservedObject var rootPresentationModel: RootPresentationModel
init(presentationModel: RootPresentationModel) {
rootPresentationModel = presentationModel
}
...
...
...
RootPresentationModel:
class RootPresentationModel: ObservableObject {
var textValue: String = "" {
didSet {
print(textValue)
}
}
}
RootBuilder:
class RootBuilder: BaseBuilder {
class func build() -> (RootView, RootInteractor) {
let interactor = RootInteractor()
let presenter = RootPresenter(presentationModel: RootPresentationModel())
let view: RootView = RootView(configuration: CustomTextFieldConfiguration.Presets.priceInput(textValue: presenter.$rootPresentationModel.textValue, placeholder: "", description: ""), interactor: interactor)
let router = RootRouter()
interactor.presenter = presenter
interactor.router = router
return (view, interactor)
}
}
(That Presets method doesn't do anything important, but just to make sure it will not raise an irrelevant question, here's the implementation):
static func priceInput(textValue: Binding<String>, placeholder: String, description: String) -> CustomTextFieldConfiguration {
return CustomTextFieldConfiguration(textValue: textValue,
placeHolder: placeholder,
description: description,
defaultDescription: description,
textAlignment: .center,
descriptionAlignment: .center,
contentAlignment: .center,
font: CustomFont.headline1))
}

import SwiftUI
struct CustomTextField: View {
#EnvironmentObject var config: CustomTextFieldConfiguration
#Binding var textValue: Double
var body: some View {
ZStack {
VStack {
VStack {
ZStack {
HStack {
//Number formatter forces the need for Double
TextField(config.placeHolder,
value: $textValue,
formatter: NumberFormatter(),
onEditingChanged: {_ in },
onCommit: {})
.frame(height: 52.0)
//.padding(EdgeInsets(top: 0, leading: 16 + (Image(systemName: config.detailActionImageName) != nil ? 44 : 0),bottom: 0, trailing: 16 + (config.contentAlignment == .center && Image(systemName: config.detailActionImageName) != nil ? 44 : 0)))
.background(config.backgroundColor)
.cornerRadius(config.cornerRedius)
.font(config.font)
}
}
}
}
}
}
}
class CustomTextFieldConfiguration: ObservableObject {
#Published var placeHolder: String = "place"
#Published var detailActionImageName: String = "checkmark"
#Published var contentAlignment: UnitPoint = .center
#Published var backgroundColor: Color = Color(UIColor.secondarySystemBackground)
#Published var font: Font = .body
#Published var cornerRedius: CGFloat = CGFloat(5)
#Published var description: String = ""
#Published var defaultDescription: String = ""
#Published var textAlignment: UnitPoint = .center
#Published var descriptionAlignment: UnitPoint = .center
init() {
}
init(placeHolder: String, description: String, defaultDescription: String, textAlignment: UnitPoint,descriptionAlignment: UnitPoint,contentAlignment: UnitPoint, font:Font) {
self.placeHolder = placeHolder
self.description = description
self.defaultDescription = defaultDescription
self.textAlignment = textAlignment
self.descriptionAlignment = descriptionAlignment
self.contentAlignment = contentAlignment
self.font = font
}
struct Presets {
static func priceInput(placeholder: String, description: String) -> CustomTextFieldConfiguration {
return CustomTextFieldConfiguration(placeHolder: placeholder, description: description,defaultDescription: description,textAlignment: .center,descriptionAlignment: .center,contentAlignment: .center, font:Font.headline)
}
}
}
struct RootView: View {
#ObservedObject var configuration: CustomTextFieldConfiguration
//var interactor: RootInteractorProtocol!
#Environment(\.colorScheme) private var colorScheme
#Binding var textValue: Double
var body: some View {
HStack {
Spacer(minLength: 40)
VStack(alignment: .trailing) {
CustomTextField(textValue: $textValue).environmentObject(configuration)
Text("\(textValue)")
}
Spacer(minLength: 40)
}
}
}
//RootPresenter is a class #ObservedObject only works properly in SwiftUI Views/struct
class RootPresenter//: BasePresenter
{
//Won't work can't chain ObservableObjects
// var rootPresentationModel: RootPresentationModel
//
// init(presentationModel: RootPresentationModel) {
// rootPresentationModel = presentationModel
// }
}
class RootPresentationModel: ObservableObject {
#Published var textValue: Double = 12 {
didSet {
print(textValue)
}
}
}
struct NewView: View {
//Must be observed directly
#StateObject var vm: RootPresentationModel = RootPresentationModel()
//This cannot be Observed
let presenter: RootPresenter = RootPresenter()
var body: some View {
RootView(configuration: CustomTextFieldConfiguration.Presets.priceInput(placeholder: "", description: ""), textValue: $vm.textValue//, interactor: interactor
)
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView()
}
}

Related

SwiftUI: Pass an ObservableObject's property into another class's initializer

How do I pass a property from an ObservedObject in a View, to another class's initializer in the same View? I get an error with my ObservedObject:
Cannot use instance member 'project' within property initializer; property initializers run before 'self' is available
The reason I want to do this is I have a class which has properties that depend on a value from the ObservedObject.
For example, I have an ObservedObject called project. I want to use the property, project.totalWordsWritten, to change the session class's property, session.totalWordCountWithSession:
struct SessionView: View {
#Binding var isPresented: Bool
#ObservedObject var project: Project
// How to pass in project.totalWordsWritten from ObservedObject project to totalWordCount?
#StateObject var session:Session = Session(startDate: Date(), sessionWordCount: 300, totalWordCount: 4000)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Form {
Section {
Text("Count")
HStack {
Text("Session word count")
TextField("", value: $session.sessionWordCount, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
}
HStack {
// Changing text field here should change the session count above
Text("Total word count")
TextField("", value: $session.totalWordCountWithSession, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
}
}
}
}.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// Save this session into the project
project.addSession(newSession: session)
isPresented = false
}
}
}
}
}
}
struct SessionView_Previews: PreviewProvider {
static var previews: some View {
SessionView(isPresented: .constant(true), project: Project(title: "TestProject", startWordCount: 0))
}
}
Below is the rest of the example:
HomeView.swift
import SwiftUI
struct HomeView: View {
#State private var showingSessionPopover:Bool = false
#StateObject var projectItem:Project = Project(title: "Test Project", startWordCount: 4000)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text(projectItem.title).font(Font.custom("OpenSans-Regular", size: 18))
.fontWeight(.bold)
Text("Count today: \(projectItem.wordsWrittenToday)")
Text("Total: \(projectItem.totalWordsWritten)")
}
.toolbar {
ToolbarItem {
Button(action: {
showingSessionPopover = true
}, label: {
Image(systemName: "calendar").imageScale(.large)
}
)
}
}
}.popover(isPresented: $showingSessionPopover) {
SessionView(isPresented: $showingSessionPopover, project: projectItem)
}
}
}
Session.swift:
import Foundation
import SwiftUI
class Session: Identifiable, ObservableObject {
init(startDate:Date, sessionWordCount:Int, totalWordCount: Int) {
self.startDate = startDate
self.endDate = Calendar.current.date(byAdding: .minute, value: 30, to: startDate) ?? Date()
self.sessionWordCount = sessionWordCount
self.totalWordCount = totalWordCount
self.totalWordCountWithSession = self.totalWordCount + sessionWordCount
}
var id: UUID = UUID()
#Published var startDate:Date
#Published var endDate:Date
var totalWordCount: Int
var sessionWordCount:Int
#Published var totalWordCountWithSession:Int {
didSet {
sessionWordCount = totalWordCountWithSession - totalWordCount
}
}
}
Project.swift
import SwiftUI
class Project: Identifiable, ObservableObject {
var id: UUID = UUID()
#Published var title:String
var sessions:[Session] = []
#Published var wordsWrittenToday:Int = 0
#Published var totalWordsWritten:Int = 0
#Published var startWordCount:Int
init(title:String,startWordCount:Int) {
self.title = title
self.startWordCount = startWordCount
self.calculateDailyAndTotalWritten()
}
// Create a new session
func addSession(newSession:Session) {
sessions.append(newSession)
calculateDailyAndTotalWritten()
}
// Re-calculate how many
// today and in total for the project
func calculateDailyAndTotalWritten() {
wordsWrittenToday = 0
totalWordsWritten = startWordCount
for session in sessions {
if (Calendar.current.isDateInToday(session.startDate)) {
wordsWrittenToday += session.sessionWordCount
}
totalWordsWritten += session.sessionWordCount
}
}
}
You can use the StateObject initializer in init:
struct SessionView: View {
#Binding var isPresented: Bool
#ObservedObject var project: Project
#StateObject var session:Session = Session(startDate: Date(), sessionWordCount: 300, totalWordCount: 4000)
init(isPresented: Binding<Bool>, project: Project, session: Session) {
_isPresented = isPresented
_session = StateObject(wrappedValue: Session(startDate: Date(), sessionWordCount: 300, totalWordCount: project.totalWordsWritten))
self.project = project
}
var body: some View {
Text("Hello, world")
}
}
Note that the documentation says:
You don’t call this initializer directly
But, it has been confirmed by SwiftUI engineers in WWDC labs that this is a legitimate technique. What runs in wrappedValue is an autoclosure and only runs on the first init of StateObject, so you don't have to be concerned that every time your View updates that it will run.
In general, though, it's a good idea to try to avoid doing things in the View's init. You could consider instead, for example, using something like task or onAppear to set the value and just put a placeholder value in at first.

SwiftUI makes UIViewRepresentable flicker while updating

I am rewriting the existing UIKit cell with SwiftUI. ChartView inside of it I wrapped with UIViewRepresentable because it is out of scope right now to rewrite it as well. The data for the chart I am getting async and update the view once I have it. The viewModel for the chart I marked as #Published so SwiftUI knows when to update the view. The problem is the view flickers once the update is there.
View:
struct DashboardWatchlistView: View {
#ObservedObject var viewModel: DashboardWatchlistViewModel
var body: some View {
VStack(alignment: .center) {
ZStack {
HStack(spacing: 4) {
HStack(spacing: 16) {
Image(systemName: "person")
VStack(alignment: .leading, spacing: .designSystem(8) {
HStack {
Text(viewModel.leftTitle).font(.subheadline).fontWeight(.semibold).lineLimit(1)
}
Text(viewModel.leftSubtitle).font(.caption2).foregroundColor(Color(ThemeManager.current.neutral50))
}
}
Spacer()
if let chartViewModel = viewModel.chartViewModel {
AssetLineChartViewRepresentable(viewModel: chartViewModel).frame(width: 65, height: 20)
}
VStack(alignment: .trailing, spacing: .designSystem(.extraSmall3)) {
percentChangeView(assetPercentChangeType: viewModel.assetPercentChangeType)
Text(viewModel.rightSubtitle).font(.caption2).foregroundColor(Color(ThemeManager.current.neutral50))
}.padding(.leading, .designSystem(.medium))
}
.padding([.leading, .trailing])
if !viewModel.hideSeparator {
VStack {
Spacer()
Divider().padding(.leading)
}
}
}
}
}
}
ViewModel:
final class DashboardWatchlistViewModel: ObservableObject {
#Published var logoURL: URL?
#Published var leftTitle = ""
#Published var leftSubtitle = ""
#Published var rightTitle = ""
#Published var rightSubtitle = ""
#Published var percentageChange: Decimal = .zero
#Published var placeholderColor = ""
#Published var corners: Corners = .none
#Published var assetPercentChangeType: AssetPercentChangeType = .zero
#Published var hideSeparator = false
#Published var isHighlighted = false
#Published var chartViewModel: AssetLineChartViewRepresentable.ViewModel?
let chartDataEvent: AnyPublisher<BPChartData?, Never>
init(tradable: Tradable,
priceString: String,
percentageChange: Decimal,
corners: Corners,
hideSeparator: Bool,
assetPercentChangeType: AssetPercentChangeType,
chartDataEvent: AnyPublisher<BPChartData?, Never>) {
self.logoURL = tradable.logoURL
self.leftTitle = tradable.name
self.rightTitle = PercentageFormatter.default.string(from: abs(percentageChange))
self.leftSubtitle = tradable.symbol
self.rightSubtitle = priceString
self.percentageChange = percentageChange
self.placeholderColor = tradable.placeholderColor ?? ""
self.corners = corners
self.chartDataEvent = chartDataEvent
self.hideSeparator = hideSeparator
self.assetPercentChangeType = assetPercentChangeType
}
}
Cell where I set the chartViewModel:
final class DashboardWatchlistCell: UICollectionViewCell, HostingCell {
var hostingController: UIHostingController<DashboardWatchlistView>?
private var dashboardWatchlistView: DashboardWatchlistView?
private var viewModel: DashboardWatchlistViewModel?
private var cancellables = Set<AnyCancellable>()
override var isHighlighted: Bool {
didSet {
viewModel?.isHighlighted = isHighlighted
}
}
func configure(with viewModel: DashboardWatchlistViewModel) {
self.dashboardWatchlistView = DashboardWatchlistView(viewModel: viewModel)
self.viewModel = viewModel
// HERE I SET THE VIEWMODEL
viewModel.chartDataEvent.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { chartData in
viewModel.chartViewModel = AssetLineChartViewRepresentable.ViewModel(chartData: chartData, percentageChange: viewModel.percentageChange)
}
.store(in: &cancellables)
}
func addHostedView(to parent: UIViewController) {
guard let dashboardWatchlistView = dashboardWatchlistView else { return }
addHostedView(dashboardWatchlistView, to: parent)
backgroundColor = .clear
}
}

How to create bindable custom objects in SWIFT? (conform with ObservableObject)

XCode Version 12.5 (12E262) - Swift 5
To simplify this example, I've created a testObj class and added a few items to an array.
Let's pretend that I want to render buttons on the screen (see preview below), once you click on the button, it should set testObj.isSelected = true which triggers the button to change the background color.
I know it's changing the value to true, however is not triggering the button to change its color.
Here's the example:
//
// TestView.swift
//
import Foundation
import SwiftUI
import Combine
struct TestView: View {
#State var arrayOfTestObj:[testObj] = [
testObj(label: "test1"),
testObj(label: "test2"),
testObj(label: "test3")
]
var body: some View {
VStack {
ForEach(arrayOfTestObj, id: \.id) { o in
HStack {
Text(o.label)
.width(200)
.padding(20)
.background(Color.red.opacity(o.isSelected ? 0.4: 0.1))
.onTapGesture {
o.isSelected.toggle()
}
}
}
}
}
}
class testObj: ObservableObject {
let didChange = PassthroughSubject<testObj, Never>()
var id:String = UUID().uuidString {didSet {didChange.send((self))}}
var label:String = "" {didSet {didChange.send((self))}}
var value:String = "" {didSet {didChange.send((self))}}
var isSelected:Bool = false {didSet {didChange.send((self))}}
init (label:String? = "") {
self.label = label!
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
If I update the ForEach as...
ForEach($arrayOfTestObj, id: \.id) { o in
... then I get this error:
Key path value type '_' cannot be converted to contextual type '_'
How can I change testObj to make it bindable?
Any help is greatly appreciated.
struct TestView: View {
#State var arrayOfTestObj:[TestObj] = [
TestObj(label: "test1"),
TestObj(label: "test2"),
TestObj(label: "test3")
]
var body: some View {
VStack {
ForEach(arrayOfTestObj, id: \.id) { o in
//Use a row view
TestRowView(object: o)
}
}
}
}
//You can observe each object by creating a RowView
struct TestRowView: View {
//And by using this wrapper you observe changes
#ObservedObject var object: TestObj
var body: some View {
HStack {
Text(object.label)
.frame(width:200)
.padding(20)
.background(Color.red.opacity(object.isSelected ? 0.4: 0.1))
.onTapGesture {
object.isSelected.toggle()
}
}
}
}
//Classes and structs should start with a capital letter
class TestObj: ObservableObject {
//You don't have to declare didChange if you need to update manually use the built in objectDidChange
let id:String = UUID().uuidString
//#Published will notify of changes
#Published var label:String = ""
#Published var value:String = ""
#Published var isSelected:Bool = false
init (label:String? = "") {
self.label = label!
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}

How to pass data object among views so its values can be modified?

I have created an object that represents the current state of drawing:
class ColoringImageViewModel: ObservableObject {
#Published var shapeItemsByKey = [UUID: ShapeItem]()
var shapeItemKeys: [UUID] = []
var scale: CGFloat = 0
var offset: CGSize = CGSize.zero
var dragGestureMode: DragGestureEnum = DragGestureEnum.FillAreas
#Published var selectedColor: Color?
var selectedImage: String?
init(selectedImage: String) {
let svgURL = Bundle.main.url(forResource: selectedImage, withExtension: "svg")!
let _paths = SVGBezierPath.pathsFromSVG(at: svgURL)
for (index, path) in _paths.enumerated() {
let scaledBezier = ScaledBezier(bezierPath: path)
let shapeItem = ShapeItem(path: scaledBezier)
shapeItemsByKey[shapeItem.id] = shapeItem
shapeItemKeys.append(shapeItem.id)
}
}
}
The main view is composed of multiple views - one for image and one for color palette among others:
struct ColoringScreenView: View {
#ObservedObject var coloringImageViewModel : ColoringImageViewModel = ColoringImageViewModel(selectedImage: "tiger")
var body: some View {
VStack {
ColoringImageView(coloringImageViewModel: coloringImageViewModel)
ColoringImageButtonsView(coloringImageViewModel: coloringImageViewModel)
}
}
}
The ColoringImageButtonsView is supposed to modify the selected color depending on selected color:
import SwiftUI
struct ColoringImageButtonsView: View {
#ObservedObject var coloringImageViewModel : ColoringImageViewModel
var paletteColors: [PaletteColorItem] = [PaletteColorItem(color: .red), PaletteColorItem(color: .green), PaletteColorItem(color: .blue), PaletteColorItem(color: .yellow), PaletteColorItem(color: .purple), PaletteColorItem(color: .black), PaletteColorItem(color: .red), PaletteColorItem(color: .red), PaletteColorItem(color: .red)]
var body: some View {
HStack {
ForEach(paletteColors) { colorItem in
Button("blue", action: {
self.coloringImageViewModel.selectedColor = colorItem.color
print("Selected color: \(self.coloringImageViewModel.selectedColor)")
}).buttonStyle(ColorButtonStyle(color: colorItem.color))
}
}
}
}
struct ColorButtonStyle: ButtonStyle {
var color: Color
init(color: Color) {
self.color = color
}
func makeBody(configuration: Configuration) -> some View {
Circle()
.fill(color)
.frame(width: 40, height: 40, alignment: .top)
}
}
struct ColoringImageButtonsView_Previews: PreviewProvider {
static var previews: some View {
var coloringImageViewModel : ColoringImageViewModel = ColoringImageViewModel(selectedImage: "tiger")
ColoringImageButtonsView(coloringImageViewModel: coloringImageViewModel)
}
}
In ShapeView (subview of ImageView), it seels that coloringImageViewModel.selectedColor is always nil:
struct ShapeView: View {
var id: UUID
#Binding var coloringImageViewModel : ColoringImageViewModel
var body: some View {
ZStack {
var shapeItem = coloringImageViewModel.shapeItemsByKey[id]!
shapeItem.path
.fill(shapeItem.color)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { gesture in
print("Tap location: \(gesture.startLocation)")
guard let currentlySelectedColor = coloringImageViewModel.selectedColor else {return}
shapeItem.color = currentlySelectedColor
}
)
.allowsHitTesting(coloringImageViewModel.dragGestureMode == DragGestureEnum.FillAreas)
shapeItem.path.stroke(Color.black)
}
}
}
I have been reading about #Binding, #State and #ObservedObject but I haven't managed to use the property wrappers correctly in order to hold the states in a single instance of an object (ColoringImageViewModel) and modify/pass its values among multiple views. Does anyone know what is the right way to do so?
I made a Swift Playground with what I think is a simplified version of your problem. It shows how you can leverage #ObservedObject, #EnvironmentObject, #State and #Binding depending the context to achieve your goal.
If you run it you should see something like this:
Notice in the code below how the instance of ColoringImageViewModel is actually created outside of any views so that it does not get caught in the view's lifecycle.
Also check out the comments next to each piece of state data that explain the different usage scenarios.
import SwiftUI
import PlaygroundSupport
// Some global constants
let images = ["circle.fill", "triangle.fill", "square.fill"]
let colors: [Color] = [.red, .green, .blue]
/// Simplified model
class ColoringImageViewModel: ObservableObject {
#Published var selectedColor: Color?
// Use singleton pattern to manage instance outside view hierarchy
static let shared = ColoringImageViewModel()
}
/// Entry-point for the coloring tool
struct ColoringTool: View {
// We bring
#ObservedObject var model = ColoringImageViewModel.shared
var body: some View {
VStack {
ColorPalette(selection: $model.selectedColor)
// We pass a binding only to the color selection
CanvasDisplay()
.environmentObject(model)
// Inject model into CanvasDisplay's environment
Text("Tap on an image to color it!")
}
}
}
struct ColorPalette: View {
// Bindings are parameters that NEED to be modified
#Binding var selection: Color?
var body: some View {
HStack {
Text("Select a color:")
ForEach(colors, id: \.self) { color in
Rectangle()
.frame(width: 50, height: 50)
.foregroundColor(color)
.border(Color.white, width:
color == self.selection ? 3 : 0
)
.onTapGesture {
self.selection = color
}
}
}
}
}
/// Displays all images
struct CanvasDisplay: View {
// Environment objects are injected by some ancestor
#EnvironmentObject private var model: ColoringImageViewModel
var body: some View {
HStack {
ForEach(images, id: \.self) {
ImageDisplay(imageName: $0, selectedColor: self.model.selectedColor)
}
}
}
}
/// A single colored, tappable image
struct ImageDisplay: View {
let imageName: String // Constant parameter
let selectedColor: Color? // Constant parameter
#State private var imageColor: Color? // Internal variable state
var body: some View {
Image(systemName: imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(
imageColor == nil ? nil : imageColor!
)
.onTapGesture {
self.imageColor = self.selectedColor
}
}
}
PlaygroundPage.current.setLiveView(ColoringTool())
You don't show where you use ShapeView in ImageView, but taking into account logic of other provided code model should be ObservedObject
struct ShapeView: View {
var id: UUID
#ObservedObject var coloringImageViewModel : ColoringImageViewModel
// ... other code

Is it possible to pass the values of GeometryReader to a #Observableobject

I need to do calculations based on the size of the device and the width of a screen.
struct TranslatorView: View {
#ObservedObject var settings = TranslationViewModel(spacing: 4, charSize: 20)
var body: some View {
GeometryReader { geometry in
VStack{
TextField("Enter your name", text:self.$settings.translateString)
}
}
}
}
My ObservableObject can be seen below
class TranslationViewModel: ObservableObject {
#Published var translateString = ""
var ScreenSize : CGFloat = 0
var spacing : CGFloat = 4
var charSize : CGFloat = 20
init(spacing: CGFloat, charSize : CGFloat) {
self.spacing = spacing
self.charSize = charSize
}
}
I need a way to pass the geometry.size.width to my ScreenSize property but have no idea how to do this.
The simplest way is to have setter-method inside the ObservableObject which returns an EmptyView.
import SwiftUI
struct TranslatorView: View {
#ObservedObject var settings = TranslationViewModel(spacing: 4, charSize: 20)
var body: some View {
GeometryReader { geometry in
VStack{
self.settings.passWidth(geometry: geometry)
TextField("Enter your name", text:self.$settings.translateString)
}
}
}
}
class TranslationViewModel: ObservableObject {
#Published var translateString = ""
var ScreenSize : CGFloat = 0
var spacing : CGFloat = 4
var charSize : CGFloat = 20
init(spacing: CGFloat, charSize : CGFloat) {
self.spacing = spacing
self.charSize = charSize
}
func passWidth(geometry: GeometryProxy) -> EmptyView {
self.ScreenSize = geometry.size.width
return EmptyView()
}
}
Then you could implement a wrapper around GeometryReader taking content: () -> Content and a closure which gets executed every time the GeometryReader gets rerendered where you can update whatever you wish.
import SwiftUI
struct TranslatorView: View {
#ObservedObject var settings = TranslationViewModel(spacing: 4, charSize: 20)
var body: some View {
GeometryReaderEasy(callback: {
self.settings.ScreenSize = $0.size.width
}) { geometry in
TextField("Enter your name", text:self.$settings.translateString)
}
}
}
struct GeometryReaderEasy<Content: View>: View {
var callback: (GeometryProxy) -> ()
var content: (GeometryProxy) -> (Content)
private func setGeometry(geometry: GeometryProxy) -> EmptyView {
callback(geometry)
return EmptyView()
}
var body: some View {
GeometryReader { geometry in
VStack{
self.setGeometry(geometry: geometry)
self.content(geometry)
}
}
}
}
You can use a simple extension on View to allow arbitrary code execution when building your views.
extension View {
func execute(_ closure: () -> Void) -> Self {
closure()
return self
}
}
And then
var body: some View {
GeometryReader { proxy
Color.clear.execute {
self.myObject.useProxy(proxy)
}
}
}
Another option is to set the value using .onAppear
struct TranslatorView: View {
#ObservedObject var settings = TranslationViewModel(spacing: 4, charSize: 20)
var body: some View {
GeometryReader { geometry in
VStack{
TextField("Enter your name", text:self.$settings.translateString)
} .onAppear {
settings.ScreenSize = geometry.size.width
}
}
}
}