This is what I mean:
var body: some View {
Text(text)
.font(Font.openSansLight(withSize: 15))
.foregroundColor(Color(uiColor: mode.underlayTextColor))
if scale { //this is a Bool value passed to initializer
.scaledToFill() //Cannot infer contextual base in reference to member 'scaledToFill'
.minimumScaleFactor(0.5)
.lineLimit(1)
}
}
In the example above, you can use the ternary operator in the modifier parameter for .minimumScaleFactor and .lineLimit, e.g.
.minimumScaleFactor(scale ? 0.5 : 1)
.lineLimit(scale ? 1 : nil)
scaledToFill doesn't take a parameter, so you could create you're own modifier that does, e.g.
struct ScaledToFill: ViewModifier {
let scale: Bool
func body(content: Content) -> some View {
if scale {
content.scaledToFill()
} else {
content
}
}
}
extension View {
func scaledToFill(_ scale: Bool) -> some View {
modifier(ScaledToFill(scale: scale))
}
}
then use it like:
.scaledToFill(scale)
Also see this for an example of a "conditional modifier" and why not to use it.
Related
I have a custom View that's basically wrapping Text with some additional functionality. E.g. formatting the text differently based on a value in the environment.
I want to use this custom view in place of a Text, i.e. I want to be able to use modifiers like .bold() on it.
Is it possible to return a Text from a View struct?
Here is 2 ways for you:
Way 1: You can use .bold() just on your CustomView!
struct CustomTextView: View {
#Environment(\.font) var environmentFont: Font?
private let boldAllowed: Bool
let string: String
private init(boldAllowed: Bool, string: String) {
self.boldAllowed = boldAllowed
self.string = string
}
init(_ string: String) {
self.init(boldAllowed: false, string: string)
}
var body: some View {
Text(string)
.font(boldAllowed ? environmentFont?.bold() : environmentFont)
.foregroundColor(Color.red) // <--- ::: some custom work here! :::
}
func bold() -> CustomTextView {
return CustomTextView(boldAllowed: true, string: string)
}
}
use case:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
CustomTextView("Hello, World!")
.bold()
CustomTextView("Hello, World!")
}
.font(Font.title.italic())
}
}
Result:
Way 2: You can use .bold() on any View you want!
I think this is the best possible way, because it is less code and usable on any view!
struct CustomBoldTextViewModifier: ViewModifier {
#Environment(\.font) var environmentFont: Font?
func body(content: Content) -> some View {
return content
.environment(\.font, environmentFont?.bold())
}
}
extension View {
func bold() -> some View {
return self.modifier(CustomBoldTextViewModifier())
}
}
use case:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
}
.bold()
.font(Font.title.italic())
}
}
Result:
You can manually get the view's body. Although this isn't recommended, it is possible. However a better solution may be to pass in a closure to modify your text, see 2nd solution. Both answers achieve the same thing.
Solution #1 (not recommended)
struct ContentView: View {
var body: some View {
CustomText("Custom text")
.getText()
.bold()
.environment(\.font, .largeTitle)
}
}
struct CustomText: View {
private let content: String
init(_ content: String) {
self.content = content
}
var body: some View {
// Apply whatever modifiers from environment, etc.
Text(content)
}
func getText() -> Text {
body as! Text
}
}
Here, the custom text is made bold.
Solution #2 (recommended)
This example you just pass in a closure of how to modify the already modified custom text. This is what I would recommend, plus it looks a lot cleaner.
struct ContentView: View {
var body: some View {
CustomText("Custom text") { text in
text
.bold()
.environment(\.font, .largeTitle)
}
}
}
struct CustomText<Content: View>: View {
private let content: String
private let transform: (Text) -> Content
init(_ content: String, transform: #escaping (Text) -> Content) {
self.content = content
self.transform = transform
}
var body: some View {
// Apply whatever modifiers from environment, etc.
let current = Text(content)
/* ... */
return transform(current)
}
}
I want build a function stand alone from ContentView which I could use this func to initialize some value, for example in this down code I want get size of View with the function, but for unknown reason for me it returns zero, I think the background modification do not work as I wanted in this build. any help?
func viewSizeReaderFunction<Content: View>(content: Content) -> CGSize {
var sizeOfView: CGSize = CGSize()
content
.background(
GeometryReader { geometry in
Color
.clear
.onAppear() { sizeOfView = geometry.size }
})
return sizeOfView
}
let sizeOfText: CGSize = viewSizeReaderFunction(content: Text("Hello, world!"))
struct ContentView: View {
var body: some View {
Color.red
.onAppear() {
print(sizeOfText)
}
}
}
The general idea is to have the view report its size using preference, and create a view modifier to capture that. But, like #RobNapier said, the struct has to be in the view hierarchy, and so within a rendering context, to be able to talk about sizes.
struct SizeReporter: ViewModifier {
#Binding var size: CGSize
func body(content: Content) -> some View {
content
.background(GeometryReader { geo in
Color.clear
.preference(key: SizePreferenceKey.self, value: geo.size)
})
.onPreferenceChange(SizePreferenceKey.self, perform: { value in
size = value
})
}
}
And we'd need to define SizePreferenceKey:
extension SizeReporter {
private struct SizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
}
You could create a convenience method on View:
extension View {
func getSize(_ size: Binding<CGSize>) -> some View {
self.modifier(SizeReporter(size: size))
}
}
and use it like so:
#State var size: CGSize = .zero
var body: some View {
Text("hello").getSize($size)
}
Views are just data that describe view layout. They are not objects that represent the actual "view" in the way that UIView is. They do not have their own logic or state (which is why they require #State variables rather than just var).
The code you've written assigns a zero-size to sizeOfView, then creates a View struct that is immediately thrown away, and then returns sizeOfView. There is nothing that evaluates the View struct. I would expect that the compiler is giving you a warning about this, something like "result of call to background is unused."
The way you do what you're describing is with Preferences, .onPreferenceChange and usually #State. There are a lot of answers to that around Stack Overflow.
https://stackoverflow.com/search?q=%5Bswiftui%5D+size+of+view
Here's one example, note in particular the use of .hidden():
extension View {
func saveSize(handler: #escaping (CGSize) -> Void) -> some View {
return self
.background(
GeometryReader { geometry in
Color.clear
.onAppear {
handler(geometry.size)
}
})
}
}
struct ContentView: View {
#State var sizeOfText: CGSize = .zero
var body: some View {
ZStack {
Color.red
.onAppear() {
print(sizeOfText)
}
Text("Hello, world!")
.hidden()
.saveSize { sizeOfText = $0 }
}
}
}
Note that this code is slightly dangerous in that it relies on the order that onAppear gets called, and that's not promised. In practice, you generally need to handle the case where the size hasn't been set yet. This can be made more robust with Preferences, but that tends to be a lot of hassle.
I'm using an HStack to layout some elements in my view hierarchy. I'd love to be able to conditionally flip the order of the elements.
HStack {
Text("Hello")
Text("World")
}
The idea is that this will either be layouted as "Hello World" or "World Hello" depending on my view's state.
HStack itself doesn't provide any functionality for this, but it also appears to be pretty much impossible trying to pull this out of the view itself attempting to use other ViewBuilders, ForEach-based approaches, etc.
The only way I can resolve this is by actually specifying both layouts entirely, which is what I'm trying to avoid.
let isFlipped: Bool
HStack {
if isFlipped {
Text("World")
Text("Hello")
} else {
Text("Hello")
Text("World")
}
}
Here is possible generic approach for any pair of views in any container base on using ViewBuilder.
Tested with Xcode 12
struct TestHStackFlip: View {
#State private var flipped = false
var body: some View {
VStack {
HStack {
FlipGroup(if: flipped) {
Text("Text1")
Text("Text2")
}
}.animation(.default) // animatable
Divider()
Button("Flip") { self.flipped.toggle() }
}
}
}
#ViewBuilder
func FlipGroup<V1: View, V2: View>(if value: Bool,
#ViewBuilder _ content: #escaping () -> TupleView<(V1, V2)>) -> some View {
let pair = content()
if value {
TupleView((pair.value.1, pair.value.0))
} else {
TupleView((pair.value.0, pair.value.1))
}
}
#State var modifierEnabled : Bool
struct BlankModifier: ViewModifier {
func body(content: Content) -> some View {
content
}
}
extension View {
func TestModifierView() -> some View{
return self.modifier(BlankModifier())
}
}
How to apply TestModifierView only in case of modifierEnabled == true ?
#available(OSX 11.0, *)
public extension View {
#ViewBuilder
func `if`<Content: View>(_ condition: Bool, content: (Self) -> Content) -> some View {
if condition {
content(self)
} else {
self
}
}
}
#available(OSX 11.0, *)
public extension View {
#ViewBuilder
func `if`<TrueContent: View, FalseContent: View>(_ condition: Bool, ifTrue trueContent: (Self) -> TrueContent, else falseContent: (Self) -> FalseContent) -> some View {
if condition {
trueContent(self)
} else {
falseContent(self)
}
}
}
usage example ( one modifier ) :
Text("some Text")
.if(modifierEnabled) { $0.foregroundColor(.Red) }
usage example2 (two modifier chains related to condition) :
Text("some Text")
.if(modifierEnabled) { $0.foregroundColor(.red) }
else: { $0.foregroundColor(.blue).background(Color.green) }
BUT!!!!!!!!!!!
Important thing that this modifier can be reason of some indentity issues. (later you will understand this)
So in some cases better to use standard if construction
I like the solution without type erasers. It looks strict and elegant.
public extension View {
#ViewBuilder
func modify<TrueContent: View, FalseContent: View>(_ condition: Bool, ifTrue modificationForTrue: (Self) -> TrueContent, ifFalse modificationForFalse: (Self) -> FalseContent) -> some View {
if condition {
modificationForTrue(self)
} else {
modificationForFalse(self)
}
}
}
Usage
HStack {
...
}
.modify(modifierEnabled) { v in
v.font(.title)
} ifFalse: {
$0.background(Color.red) // even shorter
}
If you only plan to apply a modifier (or a chain of modifiers) consider this:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
#ViewBuilder func modifier<VM1: ViewModifier, VM2: ViewModifier>(_ condition: #autoclosure () -> Bool, applyIfTrue: VM1, applyIfFalse: VM2
) -> some View {
if condition() {
self.modifier(applyIfTrue)
} else {
self.modifier(applyIfFalse)
}
}
}
Usage is almost as simple as with regular .modifier.
...
Form {
HStack {
...
}
.modifier(modifierEnabled, applyIfTrue: CornerRotateModifier(amount: 8, anchor: .bottomLeading), applyIfFalse: EmptyModifier())
...
You can omit applyIfFalse part for conciseness and just return self.erase() if condition is false.
When creating a List view onAppear triggers for elements in that list the way you would expect: As soon as you scroll to that element the onAppear triggers. However, I'm trying to implement a horizontal list like this
ScrollView(.horizontal) {
HStack(spacing: mySpacing) {
ForEach(items) { item in
MyView(item: item)
.onAppear { \\do something }
}
}
}
Using this method the onAppear triggers for all items at once, that is to say: immediately, but I want the same behavior as for a List view. How would I go about doing this? Is there a manual way to trigger onAppear, or control when views load?
Why I want to achieve this: I have made a custom Image view that loads an image from an URL only when it appears (and substitutes a placeholder in the mean time), this works fine for a List view, but I'd like it to also work for my horizontal 'list'.
As per SwiftUI 2.0 (XCode 12 beta 1) this is finally natively solved:
In a LazyHStack (or any other grid or stack with the Lazy prefix) elements will only initialise (and therefore trigger onAppear) when they appear on screen.
Here is possible approach how to do this (tested/worked with Xcode 11.2 / iOS 13.2)
Demo: (just show dynamically first & last visible cell in scrollview)
A couple of important View extensions
extension View {
func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let rect = geometry.frame(in: space)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
extension View {
func ifVisible(in rect: CGRect, in space: CoordinateSpace, execute: #escaping (CGRect) -> Void) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let frame = geometry.frame(in: space)
if frame.intersects(rect) {
execute(frame)
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
And a demo view of how to use them with cell views being in scroll view
struct TestScrollViewOnVisible: View {
#State private var firstVisible: Int = 0
#State private var lastVisible: Int = 0
#State private var visibleRect: CGRect = .zero
var body: some View {
VStack {
HStack {
Text("<< \(firstVisible)")
Spacer()
Text("\(lastVisible) >> ")
}
Divider()
band()
}
}
func band() -> some View {
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(0..<50) { i in
self.cell(for: i)
.ifVisible(in: self.visibleRect, in: .named("my")) { rect in
print(">> become visible [\(i)]")
// do anything needed with visible rects, below is simple example
// (w/o taking into account spacing)
if rect.minX <= self.visibleRect.minX && self.firstVisible != i {
DispatchQueue.main.async {
self.firstVisible = i
}
} else
if rect.maxX >= self.visibleRect.maxX && self.lastVisible != i {
DispatchQueue.main.async {
self.lastVisible = i
}
}
}
}
}
}
.coordinateSpace(name: "my")
.rectReader(self.$visibleRect, in: .named("my"))
}
func cell(for idx: Int) -> some View {
RoundedRectangle(cornerRadius: 10)
.fill(Color.yellow)
.frame(width: 80, height: 60)
.overlay(Text("\(idx)"))
}
}
I believe what you want to achieve can be done with LazyHStack.
ScrollView {
LazyVStack {
ForEach(1...100, id: \.self) { value in
Text("Row \(value)")
.onAppear {
// Write your code for onAppear here.
}
}
}
}