Is it possible to override a view modifier from a custom view? - swift

Is it possible to override your own default modifier on a custom View? If not, is there any fancy way to adjust this without using an init?
Example
struct MainView: View {
var body: some View {
CustomView()
.font(.custom(weight: .medium, fontSize: 28)) // I want the custom view to change its' "sub"-font and use this modifier instead of using .footnote font.
}
}
struct CustomView: View {
var body: some View {
VStack {
Divider()
Text("Random")
.font(.footnote)
}
}
}
One solution is just to add an Font property in the CustomView init and use it inside the viewModifier like below. But would be gladly to know if it's possible to change it from its' parent viewModifier! I might just end up with using the solution below if it's not possible.
struct CustomView: View {
let customFont: Font = .callout
var body: some View {
VStack {
Divider()
Text("Random")
.font(customFont)
}
}
}

To make your MainView work we can use extension with custom implementation of font modifier, explicit for CustomView.
Here is a demo of approach (prepared & tested with Xcode 12.5 / iOS 14.5)
CustomView()
.font(.custom("Arial", size: 28, relativeTo: .caption))
struct CustomView: View {
private var customFont: Font = .footnote
var body: some View {
VStack {
Divider()
Text("Random")
.font(customFont)
}
}
}
extension CustomView {
func font(_ font: Font) -> some View {
var updatedView = self // make writable
updatedView.customFont = font // update in copy
return updatedView // return updated with external font
}
}

You can create one Appearance class and mention all the style property for your subview component and make an own function for all property inside the view.
Here is the demo code.
CustomViewAppearance
class CustomViewAppearance {
var customFont: Font = .footnote
var textColor: Color = .red
}
CustomView and property function.
struct CustomView: View {
private var appearance = CustomViewAppearance()
var body: some View {
VStack {
Divider()
Text("Random")
.font(appearance.customFont)
.foregroundColor(appearance.textColor)
}
}
}
extension CustomView {
func font(_ font: Font) -> some View {
self.appearance.customFont = font
return self
}
func foregroundColor(_ color: Color) -> some View {
self.appearance.textColor = color
return self
}
}
--
You can also set direct Appearance.
extension CustomView {
func appearance(_ appearance: CustomViewAppearance) -> some View {
var selfView = self
selfView.appearance = appearance
return selfView
}
}
struct MainView: View {
var body: some View {
CustomView()
.appearance(customStyle())
}
func customStyle() -> CustomViewAppearance {
let appearance = CustomViewAppearance()
appearance.customFont = .largeTitle
appearance.textColor = .yellow
return appearance
}
}

Related

SwiftUI - stack multiple method calls in parent view

I am kind of a SwiftUI newbe but my question is essentially this:
I have a view that looks like this:
struct myView: View {
var label = Text("label")
var subLabel = Text("sublabel")
var body: some View {
VStack {
label
subLabel
}
}
public func primaryColor(color: Color) -> some View {
var view = self
view.label = view.label.foregroundColor(color)
return view.id(UUID())
}
public func secondaryColor(color: Color) -> some View {
var view = self
view.subLabel = view.subLabel.foregroundColor(color)
return view.id(UUID())
}
}
And here is my problem:
In my parent view, I would like to call myView as follows
struct parentView: View {
var body: some View {
myView()
.primaryColor(color: .red)
.secondaryColor(color: .blue)
}
}
Using only one of these modifiers works fine but stacking them won't work (since they return some View ?).
I don't think that I can use standard modifiers since I have to access myView variables, which (I think) wouldn't be possible by using a ViewModifier.
Is there any way to achieve my goal or am I going on the wrong direction ?
Here is a solution for you - remove .id (it is really not needed, result will be a copy anyway):
struct myView: View {
var label = Text("label")
var subLabel = Text("sublabel")
var body: some View {
VStack {
label
subLabel
}
}
public func primaryColor(color: Color) -> Self {
var view = self
view.label = view.label.foregroundColor(color)
return view
}
public func secondaryColor(color: Color) -> Self {
var view = self
view.subLabel = view.subLabel.foregroundColor(color)
return view
}
}
and no changes in parent view
Demo prepared with Xcode 13 / iOS 15
I'd suggest a refactor where primaryColor and secondaryColor can be passed as optional parameters to your MyView (in Swift, it is common practice to capitalize type names):
struct MyView: View {
var primaryColor : Color = .primary
var secondaryColor : Color = .secondary
var body: some View {
VStack {
Text("label")
.foregroundColor(primaryColor)
Text("sublabel")
.foregroundColor(secondaryColor)
}
}
}
struct ParentView: View {
var body: some View {
MyView(primaryColor: .red, secondaryColor: .blue)
}
}
This way, the code is much shorter and more simple, and follows the general form/practices of SwiftUI.
You could also use Environment keys/values to pass the properties down, but this takes more code (shown here for just the primary color, but you could expand it to secondary as well):
private struct PrimaryColorKey: EnvironmentKey {
static let defaultValue: Color = .primary
}
extension EnvironmentValues {
var primaryColor: Color {
get { self[PrimaryColorKey.self] }
set { self[PrimaryColorKey.self] = newValue }
}
}
extension View {
func primaryColor(_ primary: Color) -> some View {
environment(\.primaryColor, primary)
}
}
struct MyView: View {
#Environment(\.primaryColor) var primaryColor : Color
var body: some View {
VStack {
Text("label")
.foregroundColor(primaryColor)
}
}
}
struct ParentView: View {
var body: some View {
MyView()
.primaryColor(.red)
}
}

My two attempts at getting a blurred background for a navigation title in SwiftUI are not working

Beginner here making a simple todo list, but trying to get a blurred background only for the navigation title. I'm trying to do this with and without a UIViewRepresentable struct. Here is my method without the UIViewRepresentable struct.
"""
struct ContentView: View {
init() {
let appearance = UINavigationBarAppearance()
appearance.backgroundEffect = UIBlurEffect(style: .regular)
UINavigationBar.appearance().standardAppearance = appearance
UITableView.appearance().backgroundColor = .clear
}
var body: some View {
VStack {
GeometryReader { geometry in
VStack {
List {
ListEntry()
}
.opacity(0.8)
.frame(height: geometry.size.height*(4/5))
}
VStack {
// empty for now
}
}
}
.background(LeavesBackgroundView())
.navigationTitle(Text("Monday, Apr 26"))
.navigationBarItems(trailing: Image(systemName: "gear"))
}
}
"""
..now with the UIViewRepresentable struct:
"""
struct theBlurView: UIViewRepresentable {
#State var style: UIBlurEffect.Style = .systemMaterial
func makeUIView(context: Context) -> UIVisualEffectView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: style))
return view
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
struct ContentView: View {
init() {
let appearance = UINavigationBarAppearance()
appearance.backgroundEffect = theBlurView(style: .regular) // error right here
UINavigationBar.appearance().standardAppearance = appearance
UITableView.appearance().backgroundColor = .clear
}
var body: some View {
VStack {
GeometryReader { geometry in
VStack {
List {
ListEntry()
}
.opacity(0.8)
.frame(height: geometry.size.height*(4/5))
}
VStack {
// empty for now
}
}
}
.background(LeavesBackgroundView())
.navigationTitle(Text("Monday, Apr 26"))
.navigationBarItems(trailing: Image(systemName: "gear"))
}
}
"""
In the second case, I get the error "Cannot assign value of type 'theBlurView' to type 'UIBlurEffect?'", but I cannot figure out a way to get them to be the same type.
In the first case, I get no error, but I get a white opaque navigation title background.
In both cases, I get this
this
where the navigation title background is white. I've also tried different material styles (.dark, .light, .systemChromeMaterial, etc) and nothing makes it blurry.
This is the kind of blur I'm trying to get
Can somebody please point me in the right direction?
If you accept a third party library:
Install SwiftUIX
Make blur with few lines of code by modify your NavBar .background(VisualEffectBlurView(blurStyle: .systemThinMaterial)) and dont forget import SwiftUIX befor using.

How can I use function type?

I want to know if there is a way to use a function Type as var, for example in this code I am trying to send a function to get triggered in a Button tap Action. Is this kind of programming even possible in Swift?
The down code is not working and It is for SwiftUI, but the question is applicable to Swift as well.
struct ContentView: View {
#State var backgroundColor: Color = Color.white
func backgroundColorFunction() { backgroundColor = Color.red }
var body: some View {
ZStack {
backgroundColor.ignoresSafeArea()
CustomView(incomingFuction: backgroundColorFunction()) // :Here
}
}
}
struct CustomView: View {
var incomingFuction: ???funcType??? = ???funcType???()
var body: some View {
Button("update Background Color") {
incomingFuction()
}
}
}
Yes, it is possible, moreover it is widely used, especially in SwiftUI.
Here is your updated code:
struct FuncContentView: View {
#State var backgroundColor: Color = Color.white
func backgroundColorFunction() { backgroundColor = Color.red }
var body: some View {
ZStack {
backgroundColor.ignoresSafeArea()
CustomView(incomingFuction: backgroundColorFunction) // << here !!
}
}
}
struct CustomView: View {
var incomingFuction: () -> () // << here !!
var body: some View {
Button("update Background Color") {
incomingFuction()
}
}
}

Change background color of TextEditor in SwiftUI

TextEditor seems to have a default white background. So the following is not working and it displayed as white instead of defined red:
var body: some View {
TextEditor(text: .constant("Placeholder"))
.background(Color.red)
}
Is it possible to change the color to a custom one?
iOS 16
You should hide the default background to see your desired one:
TextEditor(text: .constant("Placeholder"))
.scrollContentBackground(.hidden) // <- Hide it
.background(.red) // To see this
iOS 15 and below
TextEditor is backed by UITextView. So you need to get rid of the UITextView's backgroundColor first and then you can set any View to the background.
struct ContentView: View {
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
List {
TextEditor(text: .constant("Placeholder"))
.background(.red)
}
}
}
Demo
You can find my simple trick for growing TextEditor here in this answer
Pure SwiftUI solution on iOS and macOS
colorMultiply is your friend.
struct ContentView: View {
#State private var editingText: String = ""
var body: some View {
TextEditor(text: $editingText)
.frame(width: 400, height: 100, alignment: .center)
.cornerRadius(3.0)
.colorMultiply(.gray)
}
}
Update iOS 16 / SwiftUI 4.0
You need to use .scrollContentBackground(.hidden) instead of UITextView.appearance().backgroundColor = .clear
https://twitter.com/StuFFmc/status/1556561422431174656
Warning: This is an iOS 16 only so you'll probably need some if #available and potentially two different TextEditor component.
extension View {
/// Layers the given views behind this ``TextEditor``.
func textEditorBackground<V>(#ViewBuilder _ content: () -> V) -> some View where V : View {
self
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
.background(content())
}
}
Custom Background color with SwiftUI on macOS
On macOS, unfortunately, you have to fallback to AppKit and wrap NSTextView.
You need to declare a view that conforms to NSViewRepresentable
This should give you pretty much the same behaviour as SwiftUI's TextEditor-View and since the wrapped NSTextView does not draw its background, you can use the .background-ViewModifier to change the background
struct CustomizableTextEditor: View {
#Binding var text: String
var body: some View {
GeometryReader { geometry in
NSScrollableTextViewRepresentable(text: $text, size: geometry.size)
}
}
}
struct NSScrollableTextViewRepresentable: NSViewRepresentable {
typealias Representable = Self
// Hook this binding up with the parent View
#Binding var text: String
var size: CGSize
// Get the UndoManager
#Environment(\.undoManager) var undoManger
// create an NSTextView
func makeNSView(context: Context) -> NSScrollView {
// create NSTextView inside NSScrollView
let scrollView = NSTextView.scrollableTextView()
let nsTextView = scrollView.documentView as! NSTextView
// use SwiftUI Coordinator as the delegate
nsTextView.delegate = context.coordinator
// set drawsBackground to false (=> clear Background)
// use .background-modifier later with SwiftUI-View
nsTextView.drawsBackground = false
// allow undo/redo
nsTextView.allowsUndo = true
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
// get wrapped nsTextView
guard let nsTextView = scrollView.documentView as? NSTextView else {
return
}
// fill entire given size
nsTextView.minSize = size
// set NSTextView string from SwiftUI-Binding
nsTextView.string = text
}
// Create Coordinator for this View
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// Declare nested Coordinator class which conforms to NSTextViewDelegate
class Coordinator: NSObject, NSTextViewDelegate {
var parent: Representable // store reference to parent
init(_ textEditor: Representable) {
self.parent = textEditor
}
// delegate method to retrieve changed text
func textDidChange(_ notification: Notification) {
// check that Notification.name is of expected notification
// cast Notification.object as NSTextView
guard notification.name == NSText.didChangeNotification,
let nsTextView = notification.object as? NSTextView else {
return
}
// set SwiftUI-Binding
parent.text = nsTextView.string
}
// Pass SwiftUI UndoManager to NSTextView
func undoManager(for view: NSTextView) -> UndoManager? {
parent.undoManger
}
// feel free to implement more delegate methods...
}
}
Usage
ContenView: View {
#State private var text: String
var body: some View {
VStack {
Text("Enter your text here:")
CustomizableTextEditor(text: $text)
.background(Color.red)
}
.frame(minWidth: 600, minHeight: 400)
}
}
Edit:
Pass reference to SwiftUI UndoManager so that default undo/redo actions are available.
Wrap NSTextView in NSScrollView so that it is scrollable. Set minSize property of NSTextView to enclosing SwiftUIView-Size so that it fills the entire allowed space.
Caveat: Only first line of this custom TextEditor is clickable to enable text editing.
This works for me on macOS
extension NSTextView {
open override var frame: CGRect {
didSet {
backgroundColor = .clear
drawsBackground = true
}
}
}
struct ContentView: View {
#State var text = ""
var body: some View {
TextEditor(text: $text)
.background(Color.red)
}
Reference this answer
To achieve this visual design here is the code I used.
iOS 16
TextField(
"free_form",
text: $comment,
prompt: Text("Type your feedback..."),
axis: .vertical
)
.lineSpacing(10.0)
.lineLimit(10...)
.padding(16)
.background(Color.themeSeashell)
.cornerRadius(16)
iOS 15
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(.gray)
TextEditor(text: $comment)
.padding()
.focused($isFocused)
if !isFocused {
Text("Type your feedback...")
.padding()
}
}
.frame(height: 132)
.onAppear() {
UITextView.appearance().backgroundColor = .clear
}
You can use Mojtaba's answer (the approved answer). It works in most cases. However, if you run into this error:
"Return from initializer without initializing all stored properties"
when trying to use the init{ ... } method, try adding UITextView.appearance().backgroundColor = .clear to .onAppear{ ... } instead.
Example:
var body: some View {
VStack(alignment: .leading) {
...
}
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
Using the Introspect library, you can use .introspectTextView for changing the background color.
TextEditor(text: .constant("Placeholder"))
.cornerRadius(8)
.frame(height: 100)
.introspectTextView { textView in
textView.backgroundColor = UIColor(Color.red)
}
Result
import SwiftUI
struct AddCommentView: View {
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
VStack {
if #available(iOS 16.0, *) {
TextEditor(text: $viewModel.commentText)
.scrollContentBackground(.hidden)
} else {
TextEditor(text: $viewModel.commentText)
}
}
.background(Color.blue)
.frame(height: UIScreen.main.bounds.width / 2)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.red, lineWidth: 1)
)
}
}
It appears the UITextView.appearance().backgroundColor = .clear trick in IOS 16,
only works for the first time you open the view and the effect disappear when the second time it loads.
So we need to provide both ways in the app. Answer from StuFF mc works.
var body: some View {
if #available(iOS 16.0, *) {
mainView.scrollContentBackground(.hidden)
} else {
mainView.onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
}
// rename body to mainView
var mainView: some View {
TextEditor(text: $notes).background(Color.red)
}

How can I know if a SwiftUI Button is enabled/disabled?

There is no isEnabled property for a SwiftUI button. How can i tell if it is enabled?
In regular UIKit, i would simply do
if button.isEnabeld == true {
} else {
}
but there is no SwiftUI equivalent.
Inside a view, if you wish to react to the state set by .disabled(true), you can use:
#Environment(\.isEnabled) var isEnabled
Since the environment can be used from within a View or a ViewModifier, this can be used to change layout properties of a view based on the state set from outside.
Unfortunately, ButtonStyle cannot directly use #Environment, but you can use a ViewModifier to inject environment values into a ButtonStyle in order to use the value from within a ButtonStyle:
// First create a button style that gets the isEnabled value injected
struct MyButtonStyle: ButtonStyle {
private let isEnabled: Bool
init(isEnabled: Bool = true) {
self.isEnabled = isEnabled
}
func makeBody(configuration: Configuration) -> some View {
return configuration
.label
.background(isEnabled ? .green : .gray)
.foregroundColor(isEnabled ? .black : .white)
}
}
// Then make a ViewModifier to inject the state
struct MyButtonModifier: ViewModifier {
#Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
return content.buttonStyle(MyButtonStyle(isEnabled: isEnabled))
}
}
// Then create a convenience function to apply the modifier
extension Button {
func styled() -> some View {
ModifiedContent(content: self, modifier: MyButtonModifier())
}
}
// Finally, try out the button and watch it respond to it's state
struct ContentView: View {
var body: some View {
Button("Test", {}).styled().disabled(true)
}
}
You can use this method to inject other things into a ButtonStyle, like size category and theme.
I use it with a custom style enum that contains all the flavours of button styles found in our design system.
From outside a view you should know if you used .disabled(true) modifier.
From inside a view you can use #Environment(\.isEnabled) to get that information:
struct MyButton: View {
let action: () -> Void
#Environment(\.isEnabled) private var isEnabled
var body: some View {
Button(action: action) {
Text("Click")
}
.foregroundColor(isEnabled ? .green : .gray)
}
}
struct MyButton_Previews: PreviewProvider {
static var previews: some View {
VStack {
MyButton(action: {})
MyButton(action: {}).disabled(true)
}
}
}
The whole idea of SwiftUI, is to avoid duplication of the source of truth. You need to think differently, and consider where the source of truth is. This is where you need to go to find out the button's state. Not from the button itself.
In "Data Flow Through SwiftUI", at minute 30:50, they explain that every piece of data has a single source of truth. If your button gets its state from some #Binding, #State, #EnvironmentObject, etc, your if statement should get that information from the same place too, not from the button.
Short answer: Just use inside struct:
#Environment(\.isEnabled) private var isEnabled
Button style with:
animation on hover change
animation on disable/enable change
can be applied on any button in native way of swiftUI
you need manually set size of buttons outside of the button
usage:
#State var isDisabled = false
///.......
Button("Styled button") { isDisabled.toggle() }
.buttonStyle(ButtStyle.BigButton()) // magic inside
.frame(width: 200, height: 50)
.disabled(isDisabled)
Button("switch isDisabled") { isDisabled.toggle() }
source code:
public struct ButtStyle { }
// Added style to easy stylyng in native way for SwiftUI
#available(macOS 11.0, *)
public extension ButtStyle {
struct BigButton: ButtonStyle {
init() {
}
public func makeBody(configuration: Configuration) -> some View {
BigButtonStyleView(configuration: configuration)
}
}
}
#available(macOS 11.0, *)
struct BigButtonStyleView : View {
let configuration: ButtonStyle.Configuration
#Environment(\.isEnabled) var isEnabled // here we getting "disabled"
#State var hover : Bool = false
var body: some View {
// added animations
MainFrameMod()
.animation(.easeInOut(duration: 0.2), value: hover)
.animation(.easeInOut(duration: 0.2), value: isEnabled)
}
// added opacity on move hover change
// and disabled status
#ViewBuilder
func MainFrameMod() -> some View {
if isEnabled {
MainFrame()
.opacity(hover ? 1 : 0.8)
.onHover{ hover = $0 }
} else {
MainFrame()
.opacity(0.5)
}
}
// Main interface of button
func MainFrame() -> some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color(hex: 0xD8D8D8))
configuration.label
.foregroundColor(.black)
.font(.custom("SF Pro", size: 18))
}
}
}
As mentioned by other developers, the main idea of SwiftUI is that the UI remains synced with the data. You can perform this in many different ways. This includes #State, #EnvironmentObject, #Binding etc.
struct ContentView: View {
#State private var isEnabled: Bool = false
var body: some View {
VStack {
Button("Press me!") {
}.disabled(isEnabled)
}
.padding()
}
}