I have an existing list, but I want to be able to add new items to it. Right now, I am using #EnvironmentObject, but when I add an element to the array, the view is not updated? I've seen solutions on the internet where you use objectWillChange.send(), but as a beginner in Swift, I don't know how to manipulate it to do what I want.
Class
class ChecklistObject: ObservableObject {
#Published var description: String
#Published var complete: Bool
let ID: Int
init(_ desc: String, _ complete: Bool, ID: Int){
description = desc
self.complete = complete
self.ID = ID
}
}
class Event: ObservableObject {
#Published var Name: String
#Published var CalendarID: Int
var timeStart: Date
var timeEnd: Date
#Published var checklist = [ChecklistObject]()
#Published var checklistSize = 0
init(_ eventName: String, _ calID: Int, _ timeStart: Date, _ timeEnd: Date) {
Name = eventName
CalendarID = calID
self.timeStart = timeStart
self.timeEnd = timeEnd
logger.log("Successfully created new event")
}
func newChecklistItem(Content: String){
objectWillChange.send()
checklist.append(ChecklistObject(Content, false, ID: getChecklistSize()))
//checklistSize = checklistSize + 1
}
func getChecklistSize() -> Int {
return checklist.count
}
}
List
VStack (alignment: .leading) {
Text("Checklist")
.font(.title)
.bold()
ForEach(event.checklist.indices) { idx in
ChecklistDisplayRow()
.environmentObject(event.checklist[idx])
}
Spacer()
.frame(width: 360, height: 10)
Button(action: {
event.newChecklistItem(Content: "New item")
event.checklistSize = event.checklistSize + 1
}) {
HStack{
if #available(OSX 11.0, *) {
Image(systemName: "plus.circle")
} else {
Path{ path in
path.move(to: CGPoint(x: 10, y: 20))
path.addLine(to: CGPoint(x: 10, y:0))
path.move(to:CGPoint(x: 0, y: 10))
path.addLine(to: CGPoint(x: 20, y: 10))
}
}
Text("Add new item")
.font(.caption)
}
}
.buttonStyle(PlainButtonStyle())
}
Thanks in advance.
It's because of the version of ForEach that you are using. This version:
init(_ data: Range<Int>, content: #escaping (Int) -> Content)
"only reads the initial value of the provided data and doesn’t need to identify views across updates." (Apple's docs)
To get dynamic updating, you need to use one of the other forms of ForEach.
ForEach(event.checklist, id: \.ID) { item in
ChecklistDisplayRow()
.environmentObject(item)
}
You could even change the ID property of your ChecklistObject to id and then add Identifiable conformance.
ForEach(event.checklist) { item in
ChecklistDisplayRow()
.environmentObject(item)
}
(And though it's hard to say since we don't see the rest of your code, but does the item really need to be passed in as an EnvironmentObject or could it be done in the initializer for ChecklistDisplayRow?)
Related
I am trying to update a List using an availableModels array inside an EnvironmentObject. For some reason it doesn't refresh my view but I have no idea why. Very confused about this so help would be much appreciated, I'm new to SwiftUI and transferring data between views. I'm pretty sure that the array is updating because when I print it out the values are correct, but the list doesn't update in the ForEach loop or in the Text fields.
This is my view with the List:
import SwiftUI
var selectedModel: String = "rdps"
struct ModelSelectView: View {
#EnvironmentObject var viewModel: WeatherData
var body: some View {
NavigationView {
List{
Group {
WeatherModelHeader()
}
if !viewModel.weatherArray.isEmpty {
ForEach(viewModel.weatherArray[0].availableModels, id: \.apiName) { model in
WeatherModelCell(weatherModel: model)
}
} else {
Text("No Weather 4 u")
}
Button(action: {
fetchModelInventory(for: viewModel)
}, label: {
Text("Fetch Inventory")
})
Text(String(viewModel.weatherArray[0].availableModels[0].apiName))
Text(String(viewModel.weatherArray[0].availableModels[1].apiName))
Text(String(viewModel.weatherArray[0].availableModels[2].apiName))
Text(String(viewModel.weatherArray[0].availableModels[3].apiName))
}
.navigationTitle("Models")
.onAppear(perform: {
fetchModelInventory(for: viewModel)
print("viewModel.weatherArray.availableModels \(viewModel.weatherArray[0].availableModels)")
})
}
}
}
//Layout for the header of the list
struct WeatherModelHeader: View {
var body: some View {
HStack {
Text("Model \nName")
.bold()
.frame(width: 60, alignment: .leading)
Text("Range")
.bold()
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
Text("Resolution")
.bold()
.frame(width: 85, alignment: .leading)
.padding(.leading, 30)
}
}
}
//create the layout for the weather model list
struct WeatherModelCell: View {
let weatherModel: WeatherModel
#EnvironmentObject var viewModel: WeatherData
var body: some View {
HStack {
//need to make navlink go to correct graph model. This will be passed in the GraphViews(). Clicking this nav link will trigger the API Call for the coordinates and model.
NavigationLink(
destination: InteractiveChart()
.onAppear{
selectedModel = weatherModel.apiName
fetchData(for: viewModel)
},
label: {
Text(weatherModel.name)
.frame(width: 60, alignment: .leading)
Text("\(String(weatherModel.range)) days")
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
Text("\(String(weatherModel.resolution)) km")
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
//this triggers the api call when the model is tapped. it passes the selected model name to the variable selectedModel for the call.
})
}
}
}
And these are my models where I set up my Observable Object:
class WeatherForecastClass: ObservableObject {
//var id = UUID()
var chartMODEL: String = "Model"
var chartModelHeight: Int = 0
var chartLAT: Double = selectedCords.latitude
var chartLON: Double = selectedCords.longitude
var chartDATETIME: Date = formatDate(date: "2023-02-10 18:00")
var chartTMP: Int = 1
var chartRH: Int = 0
var chartDP: Float = 0.0
var chartTTLSnow: Float = 0.0
var chartTTLRain: Float = 0.0
var chartWindSpeed: Int = 0
var chartWindDirDegs: Int = 0
var chartWindDirString: String = ""
var availableModels: [WeatherModel] = weatherModels
}
class WeatherData: ObservableObject {
/// weather data. This is the master class.
#Published var weatherArray: [WeatherForecastClass] = [WeatherForecastClass()]
//#Published var weatherProperties: WeatherForecastClass = WeatherForecastClass()
}
And this is my function for updating the availableModels array:
import Foundation
import SwiftUI
var modelInventory: [String] = []
func fetchModelInventory(for weatherData: WeatherData) {
// #EnvironmentObject var viewModel: WeatherData
let lat = Float(selectedCords.latitude)
let lon = Float(selectedCords.longitude)
let key = "my apiKey"
//this is the url with the API call. it has the data for the call request and my API key.
let urlString = "https://spotwx.io/api.php?key=\(key)&lat=\(lat)&lon=\(lon)&model=inventory"
guard let url = URL(string: urlString) else {
return
}
print(url)
//Gets the data from the api call. tasks are async
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
print("Error")
return
}
//clear the model inventory
modelInventory = []
weatherData.weatherArray[0].availableModels = []
//decode the modelinventory csv and fill the array
if let csvString = String(data: data, encoding: .utf8) {
let lines = csvString.split(separator: "\n")
for line in lines {
let columns = line.split(separator: ",")
for column in columns {
let value = String(column)
modelInventory.append(value)
}
}
}
print("Model Inventory -----")
print(modelInventory)
if !modelInventory.isEmpty {
DispatchQueue.main.async {
for model in weatherModels {
if modelInventory.contains(model.apiName) {
weatherData.weatherArray[0].availableModels.append(model)
}
}
print(weatherData.weatherArray[0].availableModels)
}
} else {
return
}
}
task.resume()
}
I feel like I've tried everything but I no matter what the list wont update.
Here's the situation, I have a Master / Detail view set up. When navigating from the "Events" view to the Events Details view. If a user taps the "Back" button, which I have designed using "Button(action: {self.presentationMode.wrappedValue.dismiss()})..", the view will temporarily change back to the Events list, but then jumps automatically back to the details view that a user was navigating from.
Here's the code on the Events List page
import SwiftUI
import Firebase
struct EventsView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var data: [EventObject] = []
let db = Firestore.firestore()
var body: some View {
ZStack {
VStack {
List {
ForEach((self.data), id: \.self.eventID) { item in
NavigationLink(destination: EventDetail()) {
VStack {
HStack{
Text("\(item.eventDate)")
.font(.footnote)
.foregroundColor(Color("bodyText"))
Spacer()
}
HStack {
Text("\(item.eventTitle)")
.fontWeight(.bold)
.foregroundColor(Color("Charcoal"))
Spacer()
}.padding(.top, 8)
}.padding(.bottom, 16)
} // nav
}
Spacer()
}
.padding(.top, 60)
}
//Floating Navbar
ZStack {
VStack {
GeometryReader { gr in
HStack {
Button(action: {self.presentationMode.wrappedValue.dismiss()}) {
Image(systemName: "chevron.left")
.foregroundColor(Color("Charcoal"))
.padding(.leading, 16)
HStack {
Text("Explore · Disney Events")
.font(.system(size: 15))
.fontWeight(.bold)
.foregroundColor(Color("Charcoal"))
.padding()
Spacer()
}
}.frame(width: gr.size.width * 0.92, height: 48)
.background(Color("navBackground"))
.cornerRadius(8)
.shadow(color: Color("Shadow"), radius: 10, x: 2, y: 7)
}.padding(.leading, 16)
Spacer()
}
}
.padding(.top, 50)
.edgesIgnoringSafeArea(.top)
// Floating Nav Ends
}
}.onAppear(perform: self.queryEvents)
}
func queryEvents() {
self.data.removeAll()
self.db.collectionGroup("events").getDocuments() {(querySnapshot, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
for document in querySnapshot!.documents {
let id = document.documentID
let title = document.get("eventTitle") as! String
let shortDesc = document.get("eventShort") as! String
let description = document.get("eventDescription") as! String
let date = document.get("eventDate") as! Timestamp
let aDate = date.dateValue()
let formatter = DateFormatter()
formatter.dateFormat = "E, MMM d · h:mm a"
let formattedTimeZoneStr = formatter.string(from: aDate)
let address = document.get("eventAddress") as! String
let cost = document.get("eventCost") as! Double
let location = document.get("eventLocation") as! String
let webURL = document.get("eventURL") as! String
self.data.append(EventObject(id: id, title: title, shortDesc: shortDesc, description: description, date: formattedTimeZoneStr, address: address, cost: cost, location: location, webURL: webURL))
}
}
}
}
}
class EventObject: ObservableObject {
#Published var eventID: String
#Published var eventTitle: String
#Published var eventShort: String
#Published var eventDescription: String
#Published var eventDate: String
#Published var eventAddress: String
#Published var eventCost: Double
#Published var eventLocation: String
#Published var eventURL: String
init(id: String, title: String, shortDesc: String, description: String, date: String, address: String, cost: Double, location: String, webURL: String) {
eventID = id
eventTitle = title
eventShort = shortDesc
eventDescription = description
eventDate = date
eventAddress = address
eventCost = cost
eventLocation = location
eventURL = webURL
}
}
Event Details stripped down code below. I tried to take things away to search for the cause. It seems to be isolated to the Firebase call.
import SwiftUI
import Firebase
import MapKit
struct EventDetail: View {
#Environment(\.presentationMode) var presentationMode:
Binding<PresentationMode>
// var eventID: String
// var eventTitle: String
// var eventShort: String
// var eventDescription: String
// var eventDate: String
// var eventAddress: String
// var eventCost: Double
// var eventLocation: String
// var eventURL: String
var body: some View {
ZStack {
VStack {
GeometryReader { gr in
HStack {
Button(action: {self.presentationMode.wrappedValue.dismiss()}) {
Image(systemName: "chevron.left")
.foregroundColor(Color("Charcoal"))
.padding(.leading, 16)
HStack {
Text("Events · Event Details")
.font(.system(size: 15))
.fontWeight(.bold)
.foregroundColor(Color("Charcoal"))
.padding()
Spacer()
}
}.frame(width: gr.size.width * 0.92, height: 48)
.background(Color("navBackground"))
.cornerRadius(8)
.shadow(color: Color("Shadow"), radius: 10, x: 2, y: 7)
}.padding(.leading, 16)
Spacer()
}
}
.padding(.top, 50)
.edgesIgnoringSafeArea(.top)
}
}
}
Here's a video to illustrate what I'm talking about.
Dropbox Video Link
Here is a demo of possible approach based on simplified variant of your views. The idea is to use tag/selection based NavigationLink constructor and pass binding to selection to EventDetail to deactivate selection via binding and thus activate back navigation.
Note: I think that presentationMode was not designed for navigation scenario.
struct EventsView: View {
#State private var selectedItem: Int? = nil
var body: some View {
NavigationView {
List {
ForEach(0..<10, id: \.self) { item in
NavigationLink("Item \(item)", destination: EventDetail(selected: self.$selectedItem), tag: item, selection: self.$selectedItem)
}
}
}
}
}
struct EventDetail: View {
#Binding var selected: Int?
var body: some View {
VStack {
HStack {
Button(action: { self.selected = nil }) {
Image(systemName: "chevron.left")
HStack {
Text("Events · Event Details")
.padding()
Spacer()
}
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
Spacer()
}
}
}
I am trying to implement a tag list in SwiftUI but I'm unsure how to get it to wrap the tags to additional lines if the list overflows horizontally. I started with a string array called tags and within SwiftUI I loop through the array and create buttons as follows:
HStack{
ForEach(tags, id: \.self){tag in
Button(action: {}) {
HStack {
Text(tag)
Image(systemName: "xmark.circle")
}
}
.padding()
.foregroundColor(.white)
.background(Color.orange)
.cornerRadius(.infinity)
.lineLimit(1)
}
}
If the tags array is small it renders as follows:
However, if the array has more values it does this:
The behavior I am looking for is for the last tag (yellow) to wrap to the second line. I realize it is in an HStack, I was hoping I could add a call to lineLimit with a value of greater than one but it doesn't seem to change the behavior. If I change the outer HStack to a VStack, it puts each Button on a separate line, so still not quite the behavior I am trying create. Any guidance would be greatly appreciated.
Federico Zanetello shared a nice solution in his blog: Flexible layouts in SwiftUI.
The solution is a custom view called FlexibleView which computes the necessary Row's and HStack's to lay down the given elements and wrap them into multiple rows if needed.
struct _FlexibleView<Data: Collection, Content: View>: View where Data.Element: Hashable {
let availableWidth: CGFloat
let data: Data
let spacing: CGFloat
let alignment: HorizontalAlignment
let content: (Data.Element) -> Content
#State var elementsSize: [Data.Element: CGSize] = [:]
var body : some View {
VStack(alignment: alignment, spacing: spacing) {
ForEach(computeRows(), id: \.self) { rowElements in
HStack(spacing: spacing) {
ForEach(rowElements, id: \.self) { element in
content(element)
.fixedSize()
.readSize { size in
elementsSize[element] = size
}
}
}
}
}
}
func computeRows() -> [[Data.Element]] {
var rows: [[Data.Element]] = [[]]
var currentRow = 0
var remainingWidth = availableWidth
for element in data {
let elementSize = elementsSize[element, default: CGSize(width: availableWidth, height: 1)]
if remainingWidth - (elementSize.width + spacing) >= 0 {
rows[currentRow].append(element)
} else {
currentRow = currentRow + 1
rows.append([element])
remainingWidth = availableWidth
}
remainingWidth = remainingWidth - (elementSize.width + spacing)
}
return rows
}
}
Usage:
FlexibleView(
data: [
"Here’s", "to", "the", "crazy", "ones", "the", "misfits", "the", "rebels", "the", "troublemakers", "the", "round", "pegs", "in", "the", "square", "holes", "the", "ones", "who", "see", "things", "differently", "they’re", "not", "fond", "of", "rules"
],
spacing: 15,
alignment: .leading
) { item in
Text(verbatim: item)
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray.opacity(0.2))
)
}
.padding(.horizontal, model.padding)
}
Full code available at https://github.com/zntfdr/FiveStarsCodeSamples.
Ok, this is my first answer on this site, so bear with me if I commit some kind of stack overflow faux pas.
I'll post my solution, which works for a model where the tags are either present in a selectedTags set or not, and all available tags are present in an allTags set. In my solution, these are set as bindings, so they can be injected from elsewhere in the app. Also, my solution has the tags ordered alphabetically because that was easiest. If you want them ordered a different way, you'll probably need to use a different model than two independent sets.
This definitely won't work for everyone's use case, but since I couldn't find my own answer for this out there, and your question was the only place I could find mentioning the idea, I decided I would try to build something that would work for me and share it with you. Hope it helps:
struct TagList: View {
#Binding var allTags: Set<String>
#Binding var selectedTags: Set<String>
private var orderedTags: [String] { allTags.sorted() }
private func rowCounts(_ geometry: GeometryProxy) -> [Int] { TagList.rowCounts(tags: orderedTags, padding: 26, parentWidth: geometry.size.width) }
private func tag(rowCounts: [Int], rowIndex: Int, itemIndex: Int) -> String {
let sumOfPreviousRows = rowCounts.enumerated().reduce(0) { total, next in
if next.offset < rowIndex {
return total + next.element
} else {
return total
}
}
let orderedTagsIndex = sumOfPreviousRows + itemIndex
guard orderedTags.count > orderedTagsIndex else { return "[Unknown]" }
return orderedTags[orderedTagsIndex]
}
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
ForEach(0 ..< self.rowCounts(geometry).count, id: \.self) { rowIndex in
HStack {
ForEach(0 ..< self.rowCounts(geometry)[rowIndex], id: \.self) { itemIndex in
TagButton(title: self.tag(rowCounts: self.rowCounts(geometry), rowIndex: rowIndex, itemIndex: itemIndex), selectedTags: self.$selectedTags)
}
Spacer()
}.padding(.vertical, 4)
}
Spacer()
}
}
}
}
struct TagList_Previews: PreviewProvider {
static var previews: some View {
TagList(allTags: .constant(["one", "two", "three"]), selectedTags: .constant(["two"]))
}
}
extension String {
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: fontAttributes)
return size.width
}
}
extension TagList {
static func rowCounts(tags: [String], padding: CGFloat, parentWidth: CGFloat) -> [Int] {
let tagWidths = tags.map{$0.widthOfString(usingFont: UIFont.preferredFont(forTextStyle: .headline))}
var currentLineTotal: CGFloat = 0
var currentRowCount: Int = 0
var result: [Int] = []
for tagWidth in tagWidths {
let effectiveWidth = tagWidth + (2 * padding)
if currentLineTotal + effectiveWidth <= parentWidth {
currentLineTotal += effectiveWidth
currentRowCount += 1
guard result.count != 0 else { result.append(1); continue }
result[result.count - 1] = currentRowCount
} else {
currentLineTotal = effectiveWidth
currentRowCount = 1
result.append(1)
}
}
return result
}
}
struct TagButton: View {
let title: String
#Binding var selectedTags: Set<String>
private let vPad: CGFloat = 13
private let hPad: CGFloat = 22
private let radius: CGFloat = 24
var body: some View {
Button(action: {
if self.selectedTags.contains(self.title) {
self.selectedTags.remove(self.title)
} else {
self.selectedTags.insert(self.title)
}
}) {
if self.selectedTags.contains(self.title) {
HStack {
Text(title)
.font(.headline)
}
.padding(.vertical, vPad)
.padding(.horizontal, hPad)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(radius)
.overlay(
RoundedRectangle(cornerRadius: radius)
.stroke(Color(UIColor.systemBackground), lineWidth: 1)
)
} else {
HStack {
Text(title)
.font(.headline)
.fontWeight(.light)
}
.padding(.vertical, vPad)
.padding(.horizontal, hPad)
.foregroundColor(.gray)
.overlay(
RoundedRectangle(cornerRadius: radius)
.stroke(Color.gray, lineWidth: 1)
)
}
}
}
}
I found this gist which once built, looks amazing! It did exactly what I needed for making and deleting tags. Here is a sample I built for a multi platform swift app from the code.
Tagger View
struct TaggerView: View {
#State var newTag = ""
#State var tags = ["example","hello world"]
#State var showingError = false
#State var errorString = "x" // Can't start empty or view will pop as size changes
var body: some View {
VStack(alignment: .leading) {
ErrorMessage(showingError: $showingError, errorString: $errorString)
TagEntry(newTag: $newTag, tags: $tags, showingError: $showingError, errorString: $errorString)
TagList(tags: $tags)
}
.padding()
.onChange(of: showingError, perform: { value in
if value {
// Hide the error message after a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
showingError = false
}
}
})
}
}
ErrorMessage View
struct ErrorMessage: View {
#Binding var showingError: Bool
#Binding var errorString: String
var body: some View {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text(errorString)
.foregroundColor(.secondary)
.padding(.leading, -6)
}
.font(.caption)
.opacity(showingError ? 1 : 0)
.animation(.easeIn(duration: 0.3), value: showingError)
}
}
TagEntry View
struct TagEntry: View {
#Binding var newTag: String
#Binding var tags: [String]
#Binding var showingError: Bool
#Binding var errorString: String
var body: some View {
HStack {
TextField("Add Tags", text: $newTag, onCommit: {
addTag(newTag)
})
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
Image(systemName: "plus.circle")
.foregroundColor(.blue)
.onTapGesture {
addTag(newTag)
}
}
.onChange(of: newTag, perform: { value in
if value.contains(",") {
// Try to add the tag if user types a comma
newTag = value.replacingOccurrences(of: ",", with: "")
addTag(newTag)
}
})
}
/// Checks if the entered text is valid as a tag. Sets the error message if it isn't
private func tagIsValid(_ tag: String) -> Bool {
// Invalid tags:
// - empty strings
// - tags already in the tag array
let lowerTag = tag.lowercased()
if lowerTag == "" {
showError(.Empty)
return false
} else if tags.contains(lowerTag) {
showError(.Duplicate)
return false
} else {
return true
}
}
/// If the tag is valid, it is added to an array, otherwise the error message is shown
private func addTag(_ tag: String) {
if tagIsValid(tag) {
tags.append(newTag.lowercased())
newTag = ""
}
}
private func showError(_ code: ErrorCode) {
errorString = code.rawValue
showingError = true
}
enum ErrorCode: String {
case Empty = "Tag can't be empty"
case Duplicate = "Tag can't be a duplicate"
}
}
TagList View
struct TagList: View {
#Binding var tags: [String]
var body: some View {
GeometryReader { geo in
generateTags(in: geo)
.padding(.top)
}
}
/// Adds a tag view for each tag in the array. Populates from left to right and then on to new rows when too wide for the screen
private func generateTags(in geo: GeometryProxy) -> some View {
var width: CGFloat = 0
var height: CGFloat = 0
return ZStack(alignment: .topLeading) {
ForEach(tags, id: \.self) { tag in
Tag(tag: tag, tags: $tags)
.alignmentGuide(.leading, computeValue: { tagSize in
if (abs(width - tagSize.width) > geo.size.width) {
width = 0
height -= tagSize.height
}
let offset = width
if tag == tags.last ?? "" {
width = 0
} else {
width -= tagSize.width
}
return offset
})
.alignmentGuide(.top, computeValue: { tagSize in
let offset = height
if tag == tags.last ?? "" {
height = 0
}
return offset
})
}
}
}
}
Tag View
struct Tag: View {
var tag: String
#Binding var tags: [String]
#State var fontSize: CGFloat = 20.0
#State var iconSize: CGFloat = 20.0
var body: some View {
HStack {
Text(tag.lowercased())
.font(.system(size: fontSize, weight: .regular, design: .rounded))
.padding(.leading, 2)
Image(systemName: "xmark.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.red, .blue, .white)
.font(.system(size: iconSize, weight: .black, design: .rounded))
.opacity(0.7)
.padding(.leading, -5)
}
.foregroundColor(.white)
.font(.caption2)
.padding(4)
.background(Color.blue.cornerRadius(5))
.padding(4)
.onTapGesture {
tags = tags.filter({ $0 != tag })
}
}
}
And finally…
Context View
import SwiftUI
struct ContentView: View {
var body: some View {
TaggerView()
}
}
I can’t take any credit for the code but let me send a huge thanks to Alex Hay for creating and posting this.
Link to the gist code on GitHub
I hope this helps someone.
My goal here is to make a SwiftUI struct which makes a scrolling view of data in a sequence, where the data, and the data_view get put together inside the ScrollView.
I get the title error from this code on the ForEach line. The type of the array doesn't seem to matter, I set it to 'Any', and got the same error.
If said in an English sentence, what is "(_) -> _"?
"A function taking underscore, returning underscore" doesn't make any sense.
What would make this code do what I want?
Is this even the best way to do this?
(I'm planning on working it to make the code more general, such as making a variable for the view function, but I want to get a working thing before going farther)
import SwiftUI
protocol DatedOrPrioritizedStruct {
var id: UUID {get}
var priority: Float {get set} ///Priority must be set
var start: Date? {get set} ///Could be nil. Used in conjunction with priority if set.
}
struct TimeLine : View {
var data: [DatedOrPrioritizedStruct]
var body: some View {
GeometryReader { geometry in
NavigationView {
VStack {
ScrollView {
ForEach(self.data, id: \.self) { item in
//^-ERROR redline here
TaskView(task: item).frame(minWidth: 0, idealWidth: geometry.size.width * 0.80, maxWidth: geometry.size.width, alignment: .leading)
}
}.frame(minWidth: 0, maxWidth: geometry.size.width, alignment: .leading)
.navigationBarTitle("Do The First Thing", displayMode: .large)
}
}.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
}
The Task class:
public class Task: DatedOrPrioritizedStruct, Identifiable, Equatable {
public static func == (lhs: Task, rhs: Task) -> Bool {
return lhs.id == rhs.id
}
public let id = UUID()
var title: String
var creation: Date
var priority: Float
var start: Date?
var deadline: Date?
var completed: Date?
var note: String?
var recur: Double?
var duration: TimeInterval
public init (_ title: String) {
self.title = title
self.creation = Date()
self.priority = 3.0
self.start = nil
self.deadline = nil
self.completed = nil
self.note = nil
self.recur = nil
self.duration = 3600.0
}
public init (_ title: String, priority: Float) {
self.title = title
self.creation = Date()
self.priority = priority
self.start = nil
self.deadline = nil
self.completed = nil
self.note = nil
self.recur = nil
self.duration = TimeInterval(9 * 24 * 3600 + 3 * 3600 + 20 * 60)
}
}
If you replace the
TaskView(task: item).frame(minWidth: 0, idealWidth: geometry.size.width * 0.80, maxWidth: geometry.size.width, alignment: .leading)
line with a simple Text("Testing"), you'll see the error change to
Protocol type 'DatedOrPrioritizedStruct' cannot conform to 'Hashable' because only concrete types can conform to protocols
You can fix this by specifying the ForEach's identifier to be \.id:
ForEach(self.data, id: \.id) { item in
TaskView(task: item).frame(minWidth: 0, idealWidth: geometry.size.width * 0.80, maxWidth: geometry.size.width, alignment: .leading)
}
Make sure that TaskView's task is of type DatedOrPrioritizedStruct, not Task, because since your data array contains DataOrPrioritizedStruct objects, the item in the ForEach will be a DataOrPrioritizedStruct, not a Task.
Side note: Currently, Xcode often gives irrelevant errors, like in this case, and you can try to figure out the true source by simplifying your code (which I did by putting Text("Testing") in the ForEach and commenting out the TaskView line).
Edit:
To clarify, TaskView expects to receive Task type, but the ForEach iterates on an array of DataOrPrioritizedStruct, so when you pass item into TaskView there is no guarantee that the object has the data members that are unique to Task, which is why TaskView's task's type must be DataOrPrioritizedStruct, not Task.
SwiftUI layout is very different from what we are used to. Currently I'm fighting against TextFields. Specifically their touchable Area.
TextField(
.constant(""),
placeholder: Text("My text field")
)
.padding([.leading, .trailing])
.font(.body)
This results in a very small TextField (height wise)
Adding the frame modifier fixes the issue (visually)
TextField(
.constant(""),
placeholder: Text("My text field")
).frame(height: 60)
.padding([.leading, .trailing])
.font(.body)
but the touchable area remains the same.
I'm aware of the fact that the frame modifier does nothing else other than wrap the textField in another View with the specified height.
Is there any equivalent to resizable() for Image that will allow a taller TextField with wider touchable Area?
This solution only requires a #FocusState and an onTapGesture, and allows the user to tap anywhere, including the padded area, to focus the field. Tested with iOS 15.
struct MyView: View {
#Binding var text: String
#FocusState private var isFocused: Bool
var body: some View {
TextField("", text: $text)
.padding()
.background(Color.gray)
.focused($isFocused)
.onTapGesture {
isFocused = true
}
}
}
Bonus:
If you find yourself doing this on several text fields, making a custom TextFieldStyle will make things easier:
struct TappableTextFieldStyle: TextFieldStyle {
#FocusState private var textFieldFocused: Bool
func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding()
.focused($textFieldFocused)
.onTapGesture {
textFieldFocused = true
}
}
}
Then apply it to your text fields with:
TextField("", text: $text)
.textFieldStyle(TappableTextFieldStyle())
Solution with Button
If you don't mind using Introspect you can do it by saving the UITextField and calling becomeFirstResponder() on button press.
extension View {
public func textFieldFocusableArea() -> some View {
TextFieldButton { self.contentShape(Rectangle()) }
}
}
fileprivate struct TextFieldButton<Label: View>: View {
init(label: #escaping () -> Label) {
self.label = label
}
var label: () -> Label
private var textField = Weak<UITextField>(nil)
var body: some View {
Button(action: {
self.textField.value?.becomeFirstResponder()
}, label: {
label().introspectTextField {
self.textField.value = $0
}
}).buttonStyle(PlainButtonStyle())
}
}
/// Holds a weak reference to a value
public class Weak<T: AnyObject> {
public weak var value: T?
public init(_ value: T?) {
self.value = value
}
}
Example usage:
TextField(...)
.padding(100)
.textFieldFocusableArea()
Since I use this myself as well, I will keep it updated on github: https://gist.github.com/Amzd/d7d0c7de8eae8a771cb0ae3b99eab73d
New solution using ResponderChain
The Button solution will add styling and animation which might not be wanted therefore I now use a new method using my ResponderChain package
import ResponderChain
extension View {
public func textFieldFocusableArea() -> some View {
self.modifier(TextFieldFocusableAreaModifier())
}
}
fileprivate struct TextFieldFocusableAreaModifier: ViewModifier {
#EnvironmentObject private var chain: ResponderChain
#State private var id = UUID()
func body(content: Content) -> some View {
content
.contentShape(Rectangle())
.responderTag(id)
.onTapGesture {
chain.firstResponder = id
}
}
}
You'll have to set the ResponderChain as environment object in the SceneDelegate, check the README of ResponderChain for more info.
Solution Without Any 3rd Parties
Increasing the tappable area can be done without third parties:
Step1: Create a modified TextField. This is done so we can define the padding of our new TextField:
Code used from - https://stackoverflow.com/a/27066764/2217750
class ModifiedTextField: UITextField {
let padding = UIEdgeInsets(top: 20, left: 5, bottom: 0, right: 5)
override open func textRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
override open func editingRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
}
Step 2: Make the new ModifiedTexField UIViewRepresentable so we can use it SwiftUI:
struct EnhancedTextField: UIViewRepresentable {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func makeUIView(context: Context) -> ModifiedTextField {
let textField = ModifiedTextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: ModifiedTextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
let parent: EnhancedTextField
init(_ parent: EnhancedTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
Step3: Use the new EnhancedTextField wherever needed:
EnhancedTextField(placeholder: placeholder, text: $binding)
Note: To increase or decrease the tappable area just change the padding in ModifiedTextField
let padding = UIEdgeInsets(top: 20, left: 5, bottom: 0, right: 5)
A little work around but works.
struct CustomTextField: View {
#State var name = ""
#State var isFocused = false
let textFieldsize : CGFloat = 20
var textFieldTouchAbleHeight : CGFloat = 200
var body: some View {
ZStack {
HStack{
Text(name)
.font(.system(size: textFieldsize))
.lineLimit(1)
.foregroundColor(isFocused ? Color.clear : Color.black)
.disabled(true)
Spacer()
}
.frame(alignment: .leading)
TextField(name, text: $name , onEditingChanged: { editingChanged in
isFocused = editingChanged
})
.font(.system(size: isFocused ? textFieldsize : textFieldTouchAbleHeight ))
.foregroundColor(isFocused ? Color.black : Color.clear)
.frame( height: isFocused ? 50 : textFieldTouchAbleHeight , alignment: .leading)
}.frame(width: 300, height: textFieldTouchAbleHeight + 10,alignment: .leading)
.disableAutocorrection(true)
.background(Color.white)
.padding(.horizontal,10)
.padding(.vertical,10)
.border(Color.red, width: 2)
}
}
I don't know which is better for you.
so, I post two solution.
1) If you want to shrink only input area.
var body: some View {
Form {
HStack {
Spacer().frame(width: 30)
TextField("input text", text: $inputText)
Spacer().frame(width: 30)
}
}
}
2) shrink a whole form area
var body: some View {
HStack {
Spacer().frame(width: 30)
Form {
TextField("input text", text: $restrictInput.text)
}
Spacer().frame(width: 30)
}
}
iOS 15 Solution with TextFieldStyle and additional header (it can be removed if need)
extension TextField {
func customStyle(_ title: String) -> some View {
self.textFieldStyle(CustomTextFieldStyle(title))
}
}
extension SecureField {
func customStyle(_ title: String, error) -> some View {
self.textFieldStyle(CustomTextFieldStyle(title))
}
}
struct CustomTextFieldStyle : TextFieldStyle {
#FocusState var focused: Bool
let title: String
init(_ title: String) {
self.title = title
}
public func _body(configuration: TextField<Self._Label>) -> some View {
VStack(alignment: .leading) {
Text(title)
.padding(.horizontal, 12)
configuration
.focused($focused)
.frame(height: 48)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.foregroundColor(.gray)
)
}.onTapGesture {
focused = true
}
}
}
Try using an overlay with a spacer to create a larger tapable/touchable area.
Create a myText variable:
#State private var myText = ""
Then, create your TextField with the following example formatting with an overlay:
TextField("Enter myText...", text: $myText)
.padding()
.frame(maxWidth: .infinity)
.padding(.horizontal)
.shadow(color: Color(.gray), radius: 3, x: 3, y: 3)
.overlay(
HStack {
Spacer()
})
Hope this works for you!
quick workaround would be to just put TextField in a button, and it'll make keyboard open no matter where you tap (in button); I know it's not a solution but it gets the job done (sort of).