Assume I build a view like this:
struct MyView: View {
#State private var a: String
#State private var b: String
#State private var c: String
var body: some View {
VStack {
HStack {
Text(a)
// this is the central view
Text(b).font(.headline)
}
Text(c)
}
}
}
I would like the central text view (the one displaying b) to be the anchor of the layout. That is, no matter how other text values change, I would like the central text to always stay in the centre of MyView (the centre of the text element and the centre of MyView should stay identical) and the other text elements should be laid out around the central one.
How to I achieve this? I tried to look at alignment guides, but I just don't seem to understand how to use them properly.
After spending some time to learn how alignment works in detail, I managed to arrive at a solution that only uses stacks and custom alignments, with minimal alignment guides and without needing to save any intermediate state. It's purely declarative, so I am supposed this is how SwiftUI designers intended it. I still think that there might have been a better design for it, but one can work with it.
struct ContentView: View {
#State var a: String = "AAAAA"
#State var b: String = "BBBB"
#State var c: String = "CCCCCC"
var body: some View {
VStack {
ZStack(alignment: .mid) {
// create vertical and horizontal
// space to align to
HStack { Spacer() }
VStack { Spacer() }
VStack(alignment: .midX) {
Text(self.a)
HStack(alignment: .center) {
Text(self.c)
Text(self.b)
.font(.title)
.border(Color.blue)
.alignmentGuide(.midX) { d in
(d[.leading] + d[.trailing])/2
}
.alignmentGuide(.midY) { d in
(d[.top] + d[.bottom])/2
}
}
}
}
.layoutPriority(1.0)
.overlay(CrossHair().stroke(Color.pink, lineWidth: 2))
TextField("", text: self.$b).textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
fileprivate extension HorizontalAlignment {
enum MidX : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return (d[.leading] + d[.trailing])/2
}
}
static let midX = HorizontalAlignment(MidX.self)
}
fileprivate extension VerticalAlignment {
enum MidY : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return (d[.top] + d[.bottom])/2
}
}
static let midY = VerticalAlignment(MidY.self)
}
fileprivate extension Alignment {
static let mid = Alignment(horizontal: .midX, vertical: .midY)
Related
Starting with this
var body: some View {
ScrollView {
VStack(spacing: 0.0) {
Some views here
}
}
.edgesIgnoringSafeArea(.top)
}
How would I add
List(suggestions, rowContent: { text in
NavigationLink(destination: ResultsPullerView(searchText: text)) {
Text(text)
}
})
.searchable(text: $searchText)
on top if that scrollable content?
Cause no matter how I hoax this together when
#State private var suggestions: [String] = []
gets populated (non empty) the search results are not squeezed in (or, better yet, shown on top of
"Some views here"
So what I want to achieve in different terms: search field is on top, scrollable content driven by the search results is underneath, drop down with search suggestions either temporarily squeeses scrollable content down or is overlaid on top like a modal sheet.
Thanks!
If you are looking for UIKit like search behaviour you have to display your results in an overlay:
1. Let's declare a screen to display the results:
struct SearchResultsScreen: View {
#Environment(\.isSearching) private var isSearching
var results: [String]?
var body: some View {
if isSearching, let results {
if results.isEmpty {
Text("nothing to see here")
} else {
List(results, id: \.self) { fruit in
NavigationLink(destination: Text(fruit)) {
Text(fruit)
}
}
}
}
}
}
2. Let's have an ObservableObject to handle the logic:
class Search: ObservableObject {
static private let fruit = [
"Apples ๐",
"Cherries ๐",
"Pears ๐",
"Oranges ๐",
"Pineapples ๐",
"Bananas ๐"
]
#Published var text: String = ""
var results: [String]? {
if text.isEmpty {
return nil
} else {
return Self.fruit.filter({ $0.contains(text)})
}
}
}
3. And lastly lets declare the main screen where the search bar is displayed:
struct ContentView: View {
#StateObject var search = Search()
var body: some View {
NavigationView {
LinearGradient(colors: [.orange, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.overlay(SearchResultsScreen(results: search.results))
.searchable(text: $search.text)
.navigationTitle("Find that fruit")
}
}
}
clueless beginner here. Apologies if my question is formed poorly. This is the first time I ask a question of this scale and I have a hard time balancing between posting too much code for the good samaritans to read and too little to post an effective question. Huge thanks in advance!
I am trying to incorporate the textfieldalert in this post in my learner project. There are two Swift files in questions: File A (PosTextFieldAlertView) has an extension that needs to use two functions in File B (ListView).
These are the functions I need to use in File A.
func addPositive(){
let newPositive = PositiveEntity(context: viewContext)
newPositive.title = alertInput
save()
}
func save() {
do { try viewContext.save() } catch { print(error) }
}
I thought of/researched two methods: 1) duplicate the function in File A or 2) create instance ofย the view in File B that contains that functions according this post. However I ran into problems in both methods.
Duplicating the functions:
I copied the CoreData related properties in the PosTextFieldAlert struct. But now PosTextFieldAlert in the return part of the extension has the error of "Missing arguments for parameters [Core Data properties] in call". I donโt know how to set the property in the extension without referring or creating a different sets of Core Data entities.
Creating an instance of the relevant view
In the instance creation I would need to input the arguments but I donโt know how to refer to the same NSManagedObjectContext.
Code excerpts:
PosTextFieldAlertView
struct PosTextFieldAlert<Presenting>: View where Presenting: View {
var viewContext: NSManagedObjectContext
var positives: [PositiveEntity]
var targets: [TargetEntity]
#State private var alertInput = ""
// let listView = ListView(viewContext: NSManagedObjectContext, positives: PositiveEntity, negatives: NegativeEntity, targets: TargetEntity)
#Binding var isShowing: Bool
#Binding var text: String
let presenting: Presenting
let title: String
var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in
ZStack {
self.presenting
.disabled(isShowing)
VStack {
Text(self.title)
TextField(self.title, text: self.$text)
Divider()
VStack{
HStack {
Button(action: {
withAnimation {
self.isShowing.toggle()
}
}) {
Text("+")
}.padding()
Button(action: {
withAnimation {
self.isShowing.toggle()
}
}) {
Text("-")
}.padding()
}
Button(action: {
withAnimation {
self.isShowing.toggle()
}
}) {
Text("Done")
}
}
}
.padding()
.background(Color.white)
.frame(
width: deviceSize.size.width*0.7,
height: deviceSize.size.height*0.7
)
.shadow(radius: 1)
.opacity(self.isShowing ? 1 : 0)
}
}
}
func addPositive(){
let newPositive = PositiveEntity(context: viewContext)
newPositive.title = alertInput
save()
}
func save() {
do { try viewContext.save() } catch { print(error) }
}
}
extension View {
func posTextFieldAlert(isShowing: Binding<Bool>,
text: Binding<String>,
title: String) -> some View {
PosTextFieldAlert(isShowing: isShowing,
text: text,
presenting: self,
title: title)
}
}
The code in ListView
struct ListView: View {
var viewContext: NSManagedObjectContext
var positives: [PositiveEntity]
var negatives: [NegativeEntity]
var targets: [TargetEntity]
//[layout of the project]
}
The Fetchrequests in ContentView:
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(sortDescriptors: []) var targets: FetchedResults<TargetEntity>
#FetchRequest(sortDescriptors: []) var positives: FetchedResults<PositiveEntity>
#FetchRequest(sortDescriptors: []) var negatives: FetchedResults<NegativeEntity>
I parse Int object from API, but it's too long. So I want to cut it to two characters. I parse "6700000", but I want it to be "$6.7 m", or I parse "90000000", but I want to show "$90 m".
struct MillionView: View {
#State var rockets = [RocketInfo]()
var body: some View {
List(rockets) { rocket in
HStack {
Text("Cost per launch")
Spacer()
Text("$\(rocket.costPerLaunch) m")
}
}
.onAppear {
InfoApi().getRockets { rockets in
self.rockets = rockets
}
}
}
}
struct MillionView_Previews: PreviewProvider {
static var previews: some View {
MillionView()
}
}
I was inattentive, but then I realized that the answer was:
Text("$\(rocket.costPerLaunch / 1000000) m")
I just divided the value by a million and got the desired result.
With the following code performance degrades drastically as the text gets longer due to the view getting redrawn for every character typed (due to textViewDidChange). There are also a number of nasty side effects as well, such as insertion of sentence breaks likely due to autocorrect or some other mechanism. If I comment out or eliminate the textViewDidChange function, performance is much better and the side effects disappear, but then I no longer can capture the text to save it or interrogate it on the fly.
Does anyone know of a way around this? I thought that if I could just not refresh the text variable until the focus was moved to a different view, then I could capture the text before it needs to be saved. If that might work, I can't determine how to trap the focus change. I have tried implementing textViewDidEndEditing but that only gets triggered when the parent view ends editing. I'm using a save button and want to capture the text then.
I realize that there is a TextEditor view available, but it is just as slow on large text so I assume it is doing the same thing.
import SwiftUI
struct TextView: UIViewRepresentable {
#Binding var text: String
#Binding var textStyle: UIFont.TextStyle
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = UIFont.preferredFont(forTextStyle: textStyle)
textView.autocapitalizationType = .none
textView.isSelectable = true
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
}
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
init(_ text: Binding<String>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
}
}
}
Content View
import SwiftUI
struct ContentView: View {
#State private var message = ""
#State private var textStyle = UIFont.TextStyle.body
var body: some View {
ZStack(alignment: .topTrailing) {
TextView(text: $message, textStyle: $textStyle)
.padding(.horizontal)
Button(action: {
self.textStyle = (self.textStyle == .body) ? .title1 : .body
}) {
Image(systemName: "textformat")
.imageScale(.large)
.frame(width: 40, height: 40)
.foregroundColor(.white)
.background(Color.purple)
.clipShape(Circle())
}
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm trying to use TextField to change the data of an attribute of CoreData, and everything I've come up with hasn't been successful. There is a similar question (listed below), and I'm going to post the code from the correct answer to that to explain it.
struct ItemDetail: View {
#EnvironmentObject var itemStore: ItemStore
let idx: Int
var body: some View {
NavigationView {
Stepper(value: $itemStore.items[idx].inventory) {
Text("Inventory is \(self.itemStore.items[idx].inventory)")
}
// Here I would like to do this
// TextField("PlaceHolder", $itemStore.items[idx].name)
// That doesn't work... also tried
// TextField("PlaceHolder", $name) - where name is a #State String
// How can you then automaticlly assign the new value of #State name
// To $itemStore.items[idx].name?
.padding()
.navigationBarTitle(itemStore.items[idx].name)
}
}
}
Original Question:
SwiftUI #Binding doesn't refresh View
I now have it working.
struct ItemDetail: View {
#EnvironmentObject var itemStore: ItemStore
let idx: Int
// Added new #State variable
#State var name = ""
var body: some View {
NavigationView {
Stepper(value: $itemStore.items[idx].inventory) {
Text("Inventory is \(self.itemStore.items[idx].inventory)")
}
TextField("Placeholder", text: $name) {
// When the enter key is tapped, this runs.
self.itemStore.items[self.idx].name = self.name
}
.padding()
.navigationBarTitle(itemStore.items[idx].name)
}
}
}