The idea, I thought, was this: whatever is not provided yet in SwiftUI, one can get around using UIViewRepresentable. So I set out to make a UIViewRepresentable for TTTAttributedLabel.
But there is something basic missing in the API, or I am missing something basic: how can one set the "intrinsic content size" of the UIViewRepresentable?
Much like a SwiftUI Text, I would want my component to be able to set itself to a certain size considering the size of its container.
My first idea was to use intrinsic content sizes, so I created this:
class SizeRepresentableView: UILabel {
override init(frame: CGRect) {
super.init(frame: CGRect.zero)
text="hello"
backgroundColor = UIColor.systemYellow
}
override var intrinsicContentSize: CGSize {
return CGSize(width: 100, height: 100)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct SizeRepresentable: UIViewRepresentable {
func updateUIView(_ uiView: SizeRepresentableView, context: Context) {
}
typealias UIViewType = SizeRepresentableView
func makeUIView(context: UIViewRepresentableContext<SizeRepresentable>) -> SizeRepresentableView {
let v = SizeRepresentableView(frame: CGRect.zero)
return v
}
}
struct SizeRepresentableTest: View {
var body: some View {
VStack {
SizeRepresentable()
Spacer()
}
}
}
struct SizeRepresentable_Previews: PreviewProvider {
static var previews: some View {
SizeRepresentableTest()
}
}
but that does not work. The label takes up all the space, the spacer none. With a SwiftUI Text instead of my SizeRepresentable the label is neatly arranged on top and the spacer takes up all the space, as it should.
I can't seem to find any documentation about how to layout a UIViewRepresentable.
What is the way to solve this?
See: How does UIViewRepresentable size itself in relation to the UIKit control inside it?
struct SizeRepresentableTest: View {
var body: some View {
VStack {
SizeRepresentable().fixedSize()
Spacer()
}
}
}
Related
I have the following sample SwiftUI view
public struct TestView: View {
public var body: some View {
VStack(spacing: 4) {
ForEach(1..<5) { index in
Text("Row: \(index)")
Divider()
}
}
}
}
that is embedded in my AppKit app using NSHostingController
class TestViewController: NSHostingController<TestView> {
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: TestView())
}
}
I was expecting the NSHostingController to hug its SwiftUI view, instead what I get is a huge hosting controller view with the SwiftUI view centered like this (actual view is much larger, but cropped to save a bit space here):
I get the same result with NSHostingView. I also tried to make the SwiftUI view fixedSize or manually set its frame, to no avail.
Why is it not properly fitting the SwiftUI content and how can I fix it?
I ended up having to make sure the controllers NSHostingView does not create constraints for autoresizing masks.
class TestViewController: NSHostingController<TestView> {
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: TestView())
}
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
}
}
This seems to do the trick.
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
}
}
Admittedly this is a broad question, but is it possible to undo or redo text input (via iOS's UndoManager?) when using a SwiftUI TextEditor control? I've looked everywhere and was unable to find any resource focusing on this workflow combination (SwiftUI + TextEditor + UndoManager). I'm wondering given the relative immaturity of TextEditor that either this isn't possible at all, or requires some plumbing work to facilitate. Any guidance will be greatly appreciated!
Admittedly, this is a bit of a hack and non very SwiftUI-y, but it does work. Basically declare a binding in your UITextView:UIViewRepresentable to an UndoManager. Your UIViewRepresentable will set that binding to the UndoManager provided by the UITextView. Then your parent View has access to the internal UndoManager. Here's some sample code. Redo works as well although not shown here.
struct MyTextView: UIViewRepresentable {
/// The underlying UITextView. This is a binding so that a parent view can access it. You do not assign this value. It is created automatically.
#Binding var undoManager: UndoManager?
func makeUIView(context: Context) -> UITextView {
let uiTextView = UITextView()
// Expose the UndoManager to the caller. This is performed asynchronously to avoid modifying the view at an inappropriate time.
DispatchQueue.main.async {
undoManager = uiTextView.undoManager
}
return uiTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
struct ContentView: View {
/// The underlying UndoManager. Even though it looks like we are creating one here, ultimately, MyTextView will set it to its internal UndoManager.
#State private var undoManager: UndoManager? = UndoManager()
var body: some View {
NavigationView {
MyTextView(undoManager: $undoManager)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
undoManager?.undo()
} label: {
Image(systemName: "arrow.uturn.left.circle")
}
Button {
undoManager?.redo()
} label: {
Image(systemName: "arrow.uturn.right.circle")
}
}
}
}
}
}
In respect to using UIViewRepresentable as a TextView or TextField…. this approach works for undo, but not for redo it seems.
The redo button condition undoManager.canRedo seems to change appropriately. However, it doesn’t return any undone text into either the textfield or TextView
I’m now wondering is this a bug or something I’m missing in the logic?
import SwiftUI
import PlaygroundSupport
class Model: ObservableObject {
#Published var active = ""
func registerUndo(_ newValue: String, in undoManager: UndoManager?) {
let oldValue = active
undoManager?.registerUndo(withTarget: self) { target in
target.active = oldValue
}
active = newValue
}
}
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
struct ContentView: View {
#ObservedObject private var model = Model()
#Environment(\.undoManager) var undoManager
#State var text: String = ""
var body: some View {
ZStack (alignment: .bottomTrailing) {
// Testing TextView for undo & redo functionality
TextView(text: Binding<String>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) }))
HStack{
// Testing TextField for undo & redo functionality
TextField("Enter Text...", text: Binding<String>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) })).padding()
Button("Undo") {
withAnimation {
self.undoManager?.undo()
}
}.disabled(!(undoManager?.canUndo ?? false)).padding()
Button("Redo") {
withAnimation {
self.undoManager?.redo()
}
}.disabled(!(undoManager?.canRedo ?? false)).padding()
}.background(Color(UIColor.init(displayP3Red: 0.1, green: 0.3, blue: 0.3, alpha: 0.3)))
}.frame(width: 400, height: 400, alignment: .center).border(Color.black)
}
}
PlaygroundPage.current.setLiveView(ContentView())
How do you get the frame size in UIViewRepresentable?
I have a simple DrawView class that draw something and I would like to integrate with SwiftUI. This code works if I hardcode the frame size to 640x480, but is it possible to know the current frame size of ContentView?
struct ContentView: View {
var body: some View {
SwiftDrawView()
}
}
struct SwiftDrawView: UIViewRepresentable {
func makeUIView(context: Context) -> DrawView {
DrawView(frame:CGRect(x: 0, y: 0, width: 640, height: 480))
}
....
}
The Apple tutorial always use .zero and it won't work in this case.
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
SwiftDrawView(frame: proxy.frame(in: .local))
}
}
}
struct SwiftDrawView: UIViewRepresentable {
let frame: CGRect
func makeUIView(context: Context) -> DrawView {
DrawView(frame:frame)
}
....
}
The frame does not matter at UIView creation time (ie. in makeUIView), because constructed view will be resized according to final layout in container view and resulting frame will be passed in UIView.draw.
Here is complete example (Xcode 11.1) and Preview (Note: if you remove VStack in below example DrawView fills entire screen).
import SwiftUI
import UIKit
class DrawView : UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: rect)
UIColor.green.setFill()
path.fill()
}
}
struct SwiftDrawView: UIViewRepresentable {
typealias UIViewType = DrawView
func makeUIView(context: Context) -> DrawView {
DrawView()
}
func updateUIView(_ uiView: DrawView, context: UIViewRepresentableContext<SwiftDrawView>) {
// change here some DrawView properties if needed, eg. color
}
}
struct ContentView: View {
var body: some View {
VStack {
SwiftDrawView()
Text("SwiftUI native")
}
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm looking to blur a view's background but don't want to have to break out into UIKit to accomplish it (eg. a UIVisualEffectView) I'm digging through docs and got nowhere, seemingly there is no way to live-clip a background and apply effects to it. Am I wrong or looking into it the wrong way?
1. The Native SwiftUI way:
Just add .blur() modifier on anything you need to be blurry like:
Image("BG")
.blur(radius: 20)
Note the top and bottom of the view
Note that you can Group multiple views and blur them together.
2. The Visual Effect View:
You can bring the prefect UIVisualEffectView from the UIKit:
VisualEffectView(effect: UIBlurEffect(style: .dark))
With this tiny struct:
struct VisualEffectView: UIViewRepresentable {
var effect: UIVisualEffect?
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView { UIVisualEffectView() }
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) { uiView.effect = effect }
}
3. iOS 15: Materials
You can use iOS predefined materials with one line code:
.background(.ultraThinMaterial)
I haven't found a way to achieve that in SwiftUI yet, but you can use UIKit stuff via UIViewRepresentable protocol.
struct BlurView: UIViewRepresentable {
let style: UIBlurEffect.Style
func makeUIView(context: UIViewRepresentableContext<BlurView>) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: style)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(blurView, at: 0)
NSLayoutConstraint.activate([
blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
blurView.widthAnchor.constraint(equalTo: view.widthAnchor),
])
return view
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<BlurView>) {
}
}
Demo:
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
List(1...100) { item in
Rectangle().foregroundColor(Color.pink)
}
.navigationBarTitle(Text("A List"))
ZStack {
BlurView(style: .light)
.frame(width: 300, height: 300)
Text("Hey there, I'm on top of the blur")
}
}
}
}
}
I used ZStack to put views on top of it.
ZStack {
// List
ZStack {
// Blurred View
// Text
}
}
And ends up looking like this:
The simplest way is here by Richard Mullinix:
struct Blur: UIViewRepresentable {
var style: UIBlurEffect.Style = .systemMaterial
func makeUIView(context: Context) -> UIVisualEffectView {
return UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
Then just use it somewhere in your code like background:
//...
MyView()
.background(Blur(style: .systemUltraThinMaterial))
As mentioned by #mojtaba, it's very peculiar to see white shade at top of image when you set resizable() along with blur().
As simple trick is to raise the Image padding to -ve.
var body: some View {
return
ZStack {
Image("background_2").resizable()
.edgesIgnoringSafeArea(.all)
.blur(radius: 5)
.scaledToFill()
.padding(-20) //Trick: To escape from white patch #top & #bottom
}
}
Result:
New in iOS 15 , SwiftUI has a brilliantly simple equivalent to UIVisualEffectView, that combines ZStack, the background() modifier, and a range of built-in materials.
ZStack {
Image("niceLook")
Text("Click me")
.padding()
.background(.thinMaterial)
}
You can adjust the “thickness” of your material – how much of the background content shines through – by using one of several material types. From thinnest to thickest, they are:
.ultraThinMaterial
.thinMaterial
.regularMaterial
.thickMaterial
.ultraThickMaterial
I have found an interesting hack to solve this problem. We can use UIVisualEffectView to make live "snapshot" of its background. But this "snapshot" will have an applied effect of UIVisualEffectView. We can avoid applying this effect using UIViewPropertyAnimator.
I didn't find any side effect of this hack. You can find my solution here: my GitHub Gist
Code
/// A View which content reflects all behind it
struct BackdropView: UIViewRepresentable {
func makeUIView(context: Context) -> UIVisualEffectView {
let view = UIVisualEffectView()
let blur = UIBlurEffect(style: .extraLight)
let animator = UIViewPropertyAnimator()
animator.addAnimations { view.effect = blur }
animator.fractionComplete = 0
animator.stopAnimation(true)
animator.finishAnimation(at: .start)
return view
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) { }
}
/// A transparent View that blurs its background
struct BackdropBlurView: View {
let radius: CGFloat
#ViewBuilder
var body: some View {
BackdropView().blur(radius: radius)
}
}
Usage
ZStack(alignment: .leading) {
Image(systemName: "globe")
.resizable()
.frame(width: 200, height: 200)
.foregroundColor(.accentColor)
.padding()
BackdropBlurView(radius: 6)
.frame(width: 120)
}
#State private var amount: CGFLOAT = 0.0
var body: some View {
VStack{
Image("Car").resizable().blur(radius: amount, opaque: true)
}
}
Using "Opaque: true" with blur function will eliminate white noise
There is a very useful but unfortunately private (thanks Apple) class CABackdropLayer
It draws a copy of the layers below, I found it useful when using blend mode or filters, It can also be used for blur effect
Code
open class UIBackdropView: UIView {
open override class var layerClass: AnyClass {
NSClassFromString("CABackdropLayer") ?? CALayer.self
}
}
public struct Backdrop: UIViewRepresentable {
public init() {}
public func makeUIView(context: Context) -> UIBackdropView {
UIBackdropView()
}
public func updateUIView(_ uiView: UIBackdropView, context: Context) {}
}
public struct Blur: View {
public var radius: CGFloat
public var opaque: Bool
public init(radius: CGFloat = 3.0, opaque: Bool = false) {
self.radius = radius
self.opaque = opaque
}
public var body: some View {
Backdrop()
.blur(radius: radius, opaque: opaque)
}
}
Usage
struct Example: View {
var body: some View {
ZStack {
YourBelowView()
YourTopView()
.background(Blur())
.background(Color.someColor.opacity(0.4))
}
}
}
Source
Button("Test") {}
.background(Rectangle().fill(Color.red).blur(radius: 20))