SwiftUI: Best way to include optional components in a SwiftUI view - swiftui

We have a custom textfield defined in SwiftUI. This is the basic code for it:
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: Constants.cornerRadius, style: .continuous)
.stroke(isFocused ? Color.blue : hasWarning ? .red : .gray, lineWidth: Constants.lineWidth)
HStack {
Text(title)
.font(.body)
.foregroundColor(isFocused ? Color.blue : hasWarning ? .red : .gray)
.padding(.horizontal, text.isEmpty ? Constants.Padding.horizontalIsEmpty : Constants.Padding.horizontalIsNotEmpty)
.background(text.isEmpty ? Color.clear : background)
.padding(.leading, Constants.Padding.leading)
.offset(y: text.isEmpty ? Constants.Offset.isEmpty : -(frameHeight / Constants.Offset.isNotEmptyRatio))
.scaleEffect(text.isEmpty ? Constants.ScaleEffect.isEmpty : Constants.ScaleEffect.isNotEmpty, anchor: .leading)
}
HStack {
TextField("", text: $text, onEditingChanged: { inFocus in
self.isFocused = inFocus
})
.font(.body)
.padding(.horizontal, Constants.Padding.horizontal)
}
}
.frame(height: frameHeight)
.background(background)
.animation(.easeInOut(duration: Constants.Animation.easeInOut))
.padding(.top, Constants.Padding.top)
}
Currently there's no viewModel linked to this. Now we want to extend this component to be able to include one (or both of):
An internal button
An icon in the textfield itself
I want to avoid adding any logic into the view and so I initially created a viewModel that looks something like this:
struct InternalTextfieldButtonProperties {
let action: () -> Void
let labelText: String
}
class TextFieldFloatingWithBorderViewModel: ObservableObject {
let internalButtonProperties: InternalTextfieldButtonProperties?
let icon: Image?
var hasButton: Bool {
return internalButtonProperties != nil
}
var hasIcon: Bool {
return icon != nil
}
init(internalButtonProperties: InternalTextfieldButtonProperties? = nil, icon: Image? = nil) {
self.internalButtonProperties = internalButtonProperties
self.icon = icon
}
}
The idea being that you can initialise this and then use the viewModel properties within the view to display the a button and or icon. However, this all seems a bit counterproductive as the icon and button properties in the viewModel would still need to be optional, which means again unwrapping in the view, taking away some of the point of having the viewModel. Indeed, if no icon or button is required, you end up declaring a viewModel for no reason.
I wondered if there's a cleaner way of doing this?

Related

fixed text inside TextField SwiftUI

i want to put some fixed text inside TextField like "email: ....."
I tryed this:
HStack {
Image(systemName: "list.bullet").foregroundColor(.gray)
Text("email:")
TextField("", text: Binding(
get: { viewModel.email },
set: { viewModel.email = $0 }
))
.textFieldStyle(DefaultTextFieldStyle())
.frame(width: 60)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(
viewModel.emailValid() ? Color.gray : Color.red, lineWidth: 1
))
The problem is that this implementation only allows to click in specific part (inside the textField space) but i want to make all clickable to be more friendly to edit.
I tryed putting TextField and Text inside ZStack but the text entered by the user in inside te "Email text".
Maybe its possible to move the textField cursor to the end of the "email" text, or another implementation?
Thanks!
So if its mainly about activating the TextField on tap/click on any area of the element, you can use programmatic focus for TextField:
struct ContentView: View {
#State var input = ""
#FocusState var focused: Bool
var body: some View {
HStack {
Image(systemName: "list.bullet").foregroundColor(.gray)
Text("email:")
TextField("", text: $input)
.frame(width: 80)
.focused($focused)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(
true ? Color.gray : Color.red, lineWidth: 1
))
.contentShape(Rectangle()) // makes the whole element tappable
.onTapGesture {
focused = true // sets focus for textField
}
}
}

How do i implement .tag when jumping to a posiiton in a scrollview?

I have added the .id(1) to the positions in the scrollview and can get it to work as expected if i add a button inside the scrollview but i want to use a picker to jump to the .id and outside the scrollview.
Im new to this.
I have this code:
if i use this button it works as expected although its placed inside the scrollview...
Button("Jump to position") {
value.scrollTo(1)
}
This is my picker...
// Main Picker
Picker("MainTab", selection: $mainTab) {
Text("iP1").tag(1)
Text("iP2").tag(2)
Text("Logo").tag(3)
Text("Canvas").tag(4)
}
.frame(width: 400)
.pickerStyle(.segmented)
ScrollViewReader { value in
ScrollView (.vertical, showsIndicators: false) {
ZStack {
RoundedRectangle(cornerRadius: 20)
// .backgroundStyle(.ultraThinMaterial)
.foregroundColor(.black)
.opacity(0.2)
.frame(width: 350, height:185)
// .foregroundColor(.secondary)
.id(1)
There are 2 things you are mixing here:
The tag modifier is to differentiate elements among certain selectable views. i.e. Picker TabView
You can't access the proxy reader from outside unless you make it available. In other words the tag in the Picker and the ScrollViewReader does not have a direct relationship, you have to create that yourself:
import SwiftUI
struct ScrollTest: View {
#State private var mainTab = 1
#State private var scrollReader: ScrollViewProxy?
var body: some View {
// Main Picker
Picker("MainTab", selection: $mainTab) {
Text("iP1").tag(1)
Text("iP2").tag(2)
Text("Logo").tag(3)
Text("Canvas").tag(4)
}
.frame(width: 400)
.pickerStyle(.segmented)
.onChange(of: mainTab) { mainTab in
withAnimation(.linear) {
scrollReader?.scrollTo(mainTab, anchor: .top)
}
}
ScrollViewReader { value in
ScrollView (.vertical, showsIndicators: false) {
ForEach(1...4, id: \.self) { index in
ZStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(.black)
.opacity(0.2)
.frame(width: 350, height: 500)
Text("index: \(index)")
}
.id(index)
}
}
.onAppear {
scrollReader = value
}
}
}
}

SwiftUI - Custom DatePicker - Show layer or backgorund on top of all or how to do it as DatePicker

I'm trying two write a custom Datepicker so that I can modify the button style. As far as I know, SwiftUI doesn't allow to modify it. My base design based on this answer that I've already improved it.
struct CustomDatePickerView: View {
#State private var showPicker = false
#State var selectedText: String
#State var selectedDateLocal : Date = Date()
var body: some View {
VStack {
Button {
withAnimation {
showPicker.toggle()
}
} label: {
Text(selectedText)
.padding()
.padding(.horizontal)
.foregroundColor(.black)
.background(
RoundedRectangle( cornerRadius:10, style: .continuous).fill(Color.yellow.opacity(0.2))
)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(Color.black, lineWidth: 1)
)
.id(selectedText)
}
.background(
DatePicker("", selection: $selectedDateLocal, in: closedRange, displayedComponents: [.date,.hourAndMinute])
.datePickerStyle(.graphical)
.frame(width: 400, height: 400)
.clipped()
.background(Color.yellow.opacity(0.1).cornerRadius(10))
.opacity(showPicker ? 1 : 0 )
.offset(x: 0, y: 230)
).onChange(of: selectedDateLocal) { newValue in
let format = DateFormatter()
format.timeStyle = .none
format.dateStyle = .short
print("Name changed to \(format.string(from: newValue))!")
selectedText = format.string(from: newValue)
withAnimation {
showPicker.toggle()
}
}
}
}
}
It was working excellent on test view (sorry image upload failure).
When it is placed on the real application, the background/overlay was behind of other views.
I cannot change the Z-level since the UI a bit complicated.
How can we show the DatePicker displayed on background/overlay on top of everything as the DatePicker does? What is the proper way to do this?

Class _PathPoint and Class _PointQueue error without use any pods. Just click on textField in swiftui?

I have a TextField on a View. Getting below warning on tapping of a text field. Not sure Why? Below is the code used.
This is view where button is available to click. On click of this button, Bottom view will be displayed.
struct ContentView: View {
#State var cardShown = false
#State var cardDismissal = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
cardShown.toggle()
cardDismissal.toggle()
}, label: {
Text("Show Card")
.bold()
.foregroundColor(Color.white)
.background(Color.blue)
.frame(width: 200, height: 50)
})
BottomCard(cardShown: $cardShown, cardDismissal: $cardDismissal, height: 400, content: {
CardContent()
.padding()
})
}
}
}
}
This is the bottom view where the text field exists. On this text field click, getting error.
struct CardContent: View {
#State private var text = ""
var body: some View {
VStack {
Text("Photo Collage")
.bold()
.font(.system(size: 30))
.padding()
Text("You can create awesome photo grids and share them with all of your friends")
.font(.system(size: 18))
.multilineTextAlignment(.center)
TextEditor(text: $text)
.frame(height: 100)
}
.padding()
}
}
Generic View.
struct BottomCard<Content: View>: View {
let content: Content
#Binding var cardShown: Bool
#Binding var cardDismissal: Bool
let height: CGFloat
init(cardShown: Binding<Bool>, cardDismissal: Binding<Bool>, height: CGFloat, #ViewBuilder content: () -> Content) {
_cardShown = cardShown
_cardDismissal = cardDismissal
self.height = height
self.content = content()
}
var body: some View {
ZStack {
// Dimmed
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.5))
.opacity(cardShown ? 1: 0)
.animation(Animation.easeIn, value: 0.9)
.onTapGesture {
// Dismiss
dismiss()
}
// Card
VStack {
Spacer()
VStack {
content
Button(action: {
// Dismiss
dismiss()
}, label: {
Text("Dismiss")
.foregroundColor(Color.white)
.frame(width: UIScreen.main.bounds.width/2, height: 50)
.background(Color.pink)
.cornerRadius(8)
})
.padding()
}
//.background(Color(UIColor.secondarySystemBackground))
.background(Color.yellow)
.frame(height: height)
.offset(y: (cardShown && cardShown) ? 0 : 800)
.animation(Animation.default.delay(0.2), value: 0.2)
.padding(.bottom, 300)
}
}
.edgesIgnoringSafeArea(.all)
}
func dismiss() {
cardDismissal.toggle()
//self.view.endEditing(true)
DispatchQueue.main.asyncAfter(deadline: .now()+0.25){
cardShown.toggle()
}
}
}
Getting below error while tapping on textField.
objc[9303]: Class _PointQueue is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x129df7a50) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x13c7b68d8). One of the two will be used. Which one is undefined.
objc[9303]: Class _PathPoint is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x129df7a78) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x13c7b68b0). One of the two will be used. Which one is undefined.
I was also looking for an answer on this and it appears it could just be 'log noise'. I found the following in a different question.
Apple developer Quinn “The Eskimo!” # Developer Technical Support # Apple answered this question here:
This is not an error per se. Rather, it’s the Objective-C runtime
telling you that:
Two frameworks within your process implement the same class (well, in
this case classes, namely _PathPoint and _PointQueue).
The runtime will use one of them, choosing it in an unspecified way.
This can be bad but in this case it’s not. Both of the implementations
are coming from the system (well, the simulated system) and thus you’d
expect them to be in sync and thus it doesn’t matter which one the
runtime uses.
So, in this specific case, these log messages are just log noise.

SwiftUI Animate Bar Between Custom Segment Control

I have designs for a custom Tab component (Segmented Control). The implementation is pretty basic, but one of the design requirements is for the bar at the bottom to animate between the different options (move on x axis + grow to new text size).
I have the below (WIP) implementation that statically swaps the items, but I am not sure how to get the animation between the items.
Using overlay allows for the bar to dynamically take up the full width of the parent, but I wonder if there needs to be a seperate bar that animates between the items.
Here is the WIP code:
struct Tabs: View {
#Binding var selectedTab: Int
var tabs: [Tab]
init(_ selectedTab: Binding<Int>, tabs: [Tab]) {
self._selectedTab = selectedTab
self.tabs = tabs
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(self.tabs.indices) { tabIndex in
let tab = self.tabs[tabIndex]
Button(action: {
withAnimation {
self.selectedTab = tabIndex
}
}) {
Text(tab.title)
.font(.body.weight(.medium))
.foregroundColor(.blue)
.padding(.bottom, 8)
.padding(.top, 2)
.padding(.horizontal, 4)
.if(tabIndex == self.selectedTab) {
$0.overlay(
Rectangle()
.fill(Color.blue)
.frame(width: .infinity, height: 3),
alignment: .bottom
)
}
}
}
}
}
}
}
and here is the expected design (note the underline bar, that is what I need to animate).
You create a new view under each tab during selection, this will not work. For SwiftUI, these will be different views, so they won't animate the position change.
Instead, I suggest you read this great article about alignment guides, especially the Cross Stack Alignment part.
So, using alignment guides, we can bind one of the view guides, such as center, to the selected center of the tab.
But we also need to get the width somehow. I do this with GeometryReader.
struct Tabs: View {
#State var selectedTab = 0
var tabs: [Tab]
#State private var tabWidths = [Int: CGFloat]()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
VStack(alignment: .crossAlignment, spacing: 0) {
HStack(spacing: 12) {
ForEach(self.tabs.indices) { tabIndex in
let tab = self.tabs[tabIndex]
Button(action: {
withAnimation {
self.selectedTab = tabIndex
}
}) {
Text(tab.title)
.font(.body.weight(.medium))
.foregroundColor(.blue)
.padding(.bottom, 8)
.padding(.top, 2)
.padding(.horizontal, 4)
.if(tabIndex == self.selectedTab) {
$0.alignmentGuide(.crossAlignment) { d in
d[HorizontalAlignment.center]
}
}
}.sizeReader { size in
tabWidths[tabIndex] = size.width
}
}
}
Rectangle()
.fill(Color.blue)
.frame(width: tabWidths[selectedTab], height: 3)
.alignmentGuide(.crossAlignment) { d in
d[HorizontalAlignment.center]
}
}
}
}
}
extension View {
func sizeReader(_ block: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometry -> Color in
DispatchQueue.main.async { // to avoid warning
block(geometry.size)
}
return Color.clear
}
)
}
}
extension HorizontalAlignment {
private enum CrossAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let crossAlignment = HorizontalAlignment(CrossAlignment.self)
}
p.s. Don't use .frame(width: .infinity) to extend the view, use .frame(maxWidth: .infinity) instead. Yes, you must split it into two modifiers if you want to provide a static height.
p.s.s. You should use if modifier very carefully. It's fine in this case, but in most cases it will break your animation, see this article to understand why.