How could I add 'skyfloatLabelText' in my swift UI view? - swift

I would like to use skyfloatingLabelText in swift UI. Their example show like below
let textField = SkyFloatingLabelTextField(frame: CGRect(x: 10, y: 10, width: 200, height: 45))
textField.placeholder = "Name"
textField.title = "Your full name"
self.view.addSubview(textField)
I have no idea how I could implement the above code in my swift UI. Below are my swift UI.
import SwiftUI
import SkyFloatingLabelTextField
struct Login: View {
var body: some View {
VStack() {
HeaderBar()
Spacer()
HStack {
VStack {
Text("Login Page")
.fontWeight(.bold)
TextField("Name", text: Value) //default input is ok
Button(action: /*#START_MENU_TOKEN#*/{}/*#END_MENU_TOKEN#*/) {
Text("Login")
}
}
}
Spacer()
}
}
}
struct Login_Previews: PreviewProvider {
static var previews: some View {
Login()
}
}
Thanks in advance.

Like #Asperi mentioned, you need to be using UIViewRepresentable for this.
The delegate example is taken from the github example.
import SwiftUI
import SkyFloatingLabelTextField
struct SkyFloatingContentView: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
var parent: SkyFloatingContentView
init(_ parent: SkyFloatingContentView) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
if let text = textField.text {
if let floatingLabelTextField = textField as? SkyFloatingLabelTextField {
if (text.count < 3 || !text.contains("#")) {
floatingLabelTextField.errorMessage = "Invalid email"
} else {
// The error message will only disappear when we reset it to nil or empty string
floatingLabelTextField.errorMessage = ""
}
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> SkyFloatingLabelTextField {
return SkyFloatingLabelTextField(frame: CGRect(x: 10, y: 10, width: 200, height: 45))
}
func updateUIView(_ textField: SkyFloatingLabelTextField, context: Context) {
textField.placeholder = "Name"
textField.title = "Your full name"
textField.delegate = context.coordinator
}
}
struct ContentView: View {
var body: some View {
VStack() {
Spacer()
HStack {
VStack {
Text("Login Page")
.fontWeight(.bold)
SkyFloatingContentView()
Button(action: /*#START_MENU_TOKEN#*/{}/*#END_MENU_TOKEN#*/) {
Text("Login")
}
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

`ColorPicker` with active label?

Consider the following code:
struct ContentView: View {
#State var color: Color = .blue
var body: some View {
ColorPicker(selection: $color) {
Label("Pallete", systemImage: "paintpalette")
}
}
}
It brings up a color picker modal view if you tap on color circle. I would like the same to happen also for taps on the label.
These is way to use the fancy system color picker any way we like, but as of iOS 15 it will require bringing with UIKit.
Create a new view struct like this:
import SwiftUI
struct ColorPickerPanel: UIViewControllerRepresentable {
#Binding var color: Color
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let picker = UIColorPickerViewController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ picker: UIColorPickerViewController, context: Context) {
picker.selectedColor = UIColor(color)
}
class Coordinator: NSObject, UIColorPickerViewControllerDelegate {
var parent: ColorPickerPanel
init(_ pageViewController: ColorPickerPanel) {
self.parent = pageViewController
}
func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) {
parent.color = Color(uiColor: viewController.selectedColor)
}
}
}
Then use it like this:
struct ContentView: View {
#State var color: Color = .accentColor
#State var isColorPickerPresented = false
var body: some View {
VStack {
Button {
isColorPickerPresented = true
} label: {
ColorPicker(selection: $color) {
Label("Pallete", systemImage: "paintpalette")
.allowsHitTesting(true)
.accessibilityAddTraits(.isButton)
}
}
}
.sheet(isPresented: $isColorPickerPresented) {
ZStack (alignment: .topTrailing) {
ColorPickerPanel(color: $color)
Button {
isColorPickerPresented = false
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.tint, .secondary)
.font(.title)
}
.offset(x: -10, y: 10)
}
}
}
}
You may provide another to dismiss picker, of course.

UISwitch.appearance().onTintColor not working since iOS 14 [duplicate]

I've implemented a toggle after following Apple's tutorial on user input. Currently, it looks like this:
This is the code that produces this UI:
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Show Favorites only")
}
}
}
Now, I'd like the Toggle's on-color to be blue instead of green.
I tried:
Toggle(isOn: $showFavoritesOnly) {
Text("Show Favorites only")
}
.accentColor(.blue)
.foregroundColor(.blue)
.background(Color.blue)
None of these worked and I wasn't able to find any other modifiers, such as tintColor.
How do I change the color of a Toggle?
SwiftUI 3.0
Using tint
A new modifier was introduced that can also change the Toggle color:
Toggle(isOn: $isToggleOn) {
Text("Red")
Image(systemName: "paintpalette")
}
.tint(.red)
Toggle(isOn: $isToggleOn) {
Text("Orange")
Image(systemName: "paintpalette")
}
.tint(.orange)
SwiftUI 2.0
Using SwitchToggleStyle
You can now set a tint color for the on position only in SwiftUI 2.0:
Toggle(isOn: $isToggleOn) {
Text("Red")
Image(systemName: "paintpalette")
}
.toggleStyle(SwitchToggleStyle(tint: Color.red))
Toggle(isOn: $isToggleOn) {
Text("Orange")
Image(systemName: "paintpalette")
}
.toggleStyle(SwitchToggleStyle(tint: Color.orange))
SwiftUI 1.0
Using ToggleStyle
I created a new ToggleStyle to change the three colors of the Toggle (on color, off color, and the thumb).
struct ColoredToggleStyle: ToggleStyle {
var label = ""
var onColor = Color(UIColor.green)
var offColor = Color(UIColor.systemGray5)
var thumbColor = Color.white
func makeBody(configuration: Self.Configuration) -> some View {
HStack {
Text(label)
Spacer()
Button(action: { configuration.isOn.toggle() } )
{
RoundedRectangle(cornerRadius: 16, style: .circular)
.fill(configuration.isOn ? onColor : offColor)
.frame(width: 50, height: 29)
.overlay(
Circle()
.fill(thumbColor)
.shadow(radius: 1, x: 0, y: 1)
.padding(1.5)
.offset(x: configuration.isOn ? 10 : -10))
.animation(Animation.easeInOut(duration: 0.1))
}
}
.font(.title)
.padding(.horizontal)
}
}
Examples of Use
Toggle("", isOn: $toggleState)
.toggleStyle(
ColoredToggleStyle(label: "My Colored Toggle",
onColor: .green,
offColor: .red,
thumbColor: Color(UIColor.systemTeal)))
Toggle("", isOn: $toggleState2)
.toggleStyle(
ColoredToggleStyle(label: "My Colored Toggle",
onColor: .purple))
From the SwiftUI Book
Just use UIAppearance APIs:
UISwitch.appearance().onTintColor = UIColor.blue
It'll of course by default change the appearance of all the instances of UISwitch, as per UIAppearance documentation.
NOTE: Tested as of Xcode 11 beta 5.
SwiftUI 2.0 (Post WWDC-2020)
Using the new SwiftUI enhancements you can use the .toggleStyle modifier.
// Switch tinting
Toggle(isOn: $order.notifyWhenReady) {
Text("Send notification when ready")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Note this only works for iOS14/iPadOS14/macOS11 and above.
I haven't found a way to directly change a Toggle color yet but an alternative way to have a blue switch or any other custom views, is to create a custom view of your own. To make a custom blue toggle in its simplest form:
struct BlueToggle : UIViewRepresentable {
func makeUIView(context: Context) -> UISwitch {
UISwitch()
}
func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.onTintColor = UIColor.blue
}
}
struct ContentView : View {
var body: some View {
BlueToggle()
}
}
Result:
You can modify the global onTintColor for all UISwitch objects inside init().
#State var enable_dhcp = true
init()
{
UISwitch.appearance().onTintColor = .red
}
var body: some View
{
Toggle("DHCP", isOn: $enable_dhcp)
}
Building off #mohammad-reza-farahani 's solution, here is a fully uncompromising approach to getting the configurability of UISwitch with the implementation protocols if SwiftUI.
First wrap a UISwitch in a UIViewRepresentable and set the colors as you wish:
final class CustomToggleWrapper: UIViewRepresentable {
var isOn: Binding<Bool>
init(isOn: Binding<Bool>) {
self.isOn = isOn
}
func makeUIView(context: Context) -> UISwitch {
UISwitch()
}
func updateUIView(_ uiView: UISwitch, context: Context) {
// On color
uiView.onTintColor = UIColor.blue
// Off color
uiView.tintColor = UIColor.red
uiView.layer.cornerRadius = uiView.frame.height / 2
uiView.backgroundColor = UIColor.red
uiView.isOn = isOn.wrappedValue
// Update bound boolean
uiView.addTarget(self, action: #selector(switchIsChanged(_:)), for: .valueChanged)
}
#objc
func switchIsChanged(_ sender: UISwitch) {
isOn.wrappedValue = sender.isOn
}
}
Second, create a custom toggle style using the wrapped UISwitch:
struct CustomToggleStyle: ToggleStyle {
func makeBody(configuration: Self.Configuration) -> some View {
let toggle = CustomToggleWrapper(isOn: configuration.$isOn)
return HStack {
configuration.label
Spacer()
toggle
}
}
}
Implement a Toggle as you normally would, and apply your CustomToggleStyle:
struct TestView: View {
#State private var isOn: Bool = true
var body: some View {
Toggle(
isOn: $isOn
) {
Text("Test: \(String(isOn))")
}.toggleStyle(CustomToggleStyle()).padding()
}
}
Karol Kulesza and George Valkov have provided a very easy to implement solution. I just wanted to add that you can place the code below inside the app delegate's didFinishLaunching method as well.
UISwitch.appearance().onTintColor = .blue
You can also create more specific appearance configurations with
appearance(whenContainedInInstancesOf:)
See https://www.hackingwithswift.com/example-code/uikit/what-is-the-uiappearance-proxy
As the original question was just about changing the toggle on colour and not full Toggle visual customisation, I think something like this would do:
import SwiftUI
struct CustomToggle: UIViewRepresentable {
#Binding var isOn: Bool
func makeCoordinator() -> CustomToggle.Coordinator {
Coordinator(isOn: $isOn)
}
func makeUIView(context: Context) -> UISwitch {
let view = UISwitch()
view.onTintColor = UIColor.red
view.addTarget(context.coordinator, action: #selector(Coordinator.switchIsChanged(_:)), for: .valueChanged)
return view
}
func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.isOn = isOn
}
class Coordinator: NSObject {
#Binding private var isOn: Bool
init(isOn: Binding<Bool>) {
_isOn = isOn
}
#objc func switchIsChanged(_ sender: UISwitch) {
_isOn.wrappedValue = sender.isOn
}
}
}
// MARK: - Previews
struct CustomToggle_Previews: PreviewProvider {
static var previews: some View {
ViewWrapper()
}
struct ViewWrapper: View {
#State(initialValue: false) var isOn: Bool
var body: some View {
CustomToggle(isOn: $isOn)
.previewLayout(.fixed(width: 100, height: 100))
}
}
}
The easist way is setting UISwitch.appearance().onTintColor = UIColor.red before using toggle and use SwiftUI Toggle like below.
UISwitch.appearance().onTintColor = UIColor.red
...
let toggle = Toggle(isOn: $vm.dataUsePermission, label: {
Text(I18N.permit_data_usage)
.font(SwiftUI.Font.system(size: 16, weight: .regular))
})
if #available(iOS 14.0, *) {
toggle.toggleStyle(
SwitchToggleStyle(tint: Color(UIColor.m.blue500))
)
} else {
toggle.toggleStyle(SwitchToggleStyle())
}
...
You can alse use same Toggle interface in SwiftUI but different name, and change tint color.
TintableSwitch(isOn: .constant(true), label: {
Text("Switch")
})
Toggle(isOn: .constant(true), label: {
Text("Switch")
})
If only need Toggle without Label, then
TintableUISwitch(isOn: .constant(true))
Use below code.
import SwiftUI
public struct TintableSwitch<Label>: View where Label: View {
#Binding var isOn: Bool
var label: Label
public init(isOn: Binding<Bool>, #ViewBuilder label: () -> Label) {
self._isOn = isOn
self.label = label()
}
public var body: some View {
HStack {
label
Spacer()
TintableUISwitch(isOn: $isOn, onTintColor: .red) // 📌 CHANGE HERE
}
}
}
public struct TintableUISwitch: UIViewRepresentable {
#Binding var isOn: Bool
private var onTintColor: UIColor
public init(isOn: Binding<Bool>, onTintColor: UIColor = UIColor.m.blue500) {
self._isOn = isOn
self.onTintColor = onTintColor
}
public func makeUIView(context: Context) -> UISwitch {
let uiSwitch = UISwitch()
uiSwitch.addTarget(
context.coordinator,
action: #selector(Coordinator.valueChanged(_:)),
for: .valueChanged
)
uiSwitch.onTintColor = onTintColor
uiSwitch.isOn = isOn
return uiSwitch
}
public func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.isOn = isOn
}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
public class Coordinator: NSObject {
var tintableSwitch: TintableUISwitch
init(_ tintableSwitch: TintableUISwitch) {
self.tintableSwitch = tintableSwitch
}
#objc
func valueChanged(_ sender: UISwitch) {
tintableSwitch.isOn = sender.isOn
}
}
}
struct TintableSwitch_Previews: PreviewProvider {
static var previews: some View {
VStack {
TintableSwitch(isOn: .constant(true), label: {
Text("Switch")
})
Toggle(isOn: .constant(true), label: {
Text("Switch")
})
}
}
}
struct TintableUISwitch_Previews: PreviewProvider {
static var previews: some View {
TintableUISwitch(isOn: .constant(true))
}
}
You can change the toggle color in IOS 15.0 using a tint modifier.
Toggle(isOn: $isToggleOn) {
Text("Toggle")
}.tint(.red)
and below IOS 15.0, You can use toggleStyle modifier to change the toggle color but it will be depreciated in the future.
Toggle(isOn: $isToggleOn) {
Text("Toggle")
}.toggleStyle(SwitchToggleStyle(tint: .red))
I would change #Mark Moeykens answer a little bit to avoid having the button tap animation. A better solution would be:
#available(iOS 13.0, *)
struct ColoredToggleStyle: ToggleStyle {
var label = ""
var onColor = UIColor.proacPrimaryBlue.suColor
var offColor = UIColor.systemGray5.suColor
var thumbColor = Color.white
func makeBody(configuration: Self.Configuration) -> some View {
HStack {
Text(label)
Spacer()
RoundedRectangle(cornerRadius: 16, style: .circular)
.fill(configuration.isOn ? onColor : offColor)
.frame(width: 50, height: 29)
.overlay(
Circle()
.fill(thumbColor)
.shadow(radius: 1, x: 0, y: 1)
.padding(1.5)
.offset(x: configuration.isOn ? 10 : -10))
.animation(Animation.easeInOut(duration: 0.1))
.onTapGesture {
configuration.isOn.toggle()
}
}
.font(.title)
.padding(.horizontal)
}
}
This https://stackoverflow.com/a/56480720/5941807 (for now whit Xcode 11 beta 6) is a solution. To switch between to option a fast way is using the boolean instead of if/else:
showFavoritesOnly ? .red : .blue
for foreground:
Toggle(isOn: $showGreeting) {
Text("Show Favorites only").foregroundColor(showFavoritesOnly ? .blue : .gray)
}
for tint:
uiView.onTintColor = showFavoritesOnly ? UIColor.blue : UIColor.gray
In addition for custom colors: https://stackoverflow.com/a/57744208/5941807

Passing an EnvronmentObject to NSHostingControllers

I'm using a custom SwiftUI View using NSSplitViewController that takes in ViewBuilders for the two subviews. My problem is any change in state of the environment doesn't propagate to the subviews inside SplitView, but propagates to another TextView in ContentView
import SwiftUI
class AppEnvironment : ObservableObject {
#Published var value: String = "default"
}
struct ContentView: View {
#EnvironmentObject var env : AppEnvironment
var body: some View {
HStack {
Button(action: {
self.env.value = "new value"
}, label: { Text("Change value") })
Text(self.env.value)
GeometryReader { geometry in
SplitView(master: {
Text("master")
.background(Color.yellow)
}, detail: {
HStack {
Text(self.env.value) }
.background(Color.orange)
}).frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//
// Source: https://gist.github.com/HashNuke/f8895192fff1f275e66c30340f304d80
//
struct SplitView<Master: View, Detail: View>: View {
var master: Master
var detail: Detail
init(#ViewBuilder master: () -> Master, #ViewBuilder detail: () -> Detail) {
self.master = master()
self.detail = detail()
}
var body: some View {
let viewControllers = [NSHostingController(rootView: master), NSHostingController(rootView: detail)]
return SplitViewController(viewControllers: viewControllers)
}
}
struct SplitViewController: NSViewControllerRepresentable {
var viewControllers: [NSViewController]
private let splitViewResorationIdentifier = "com.company.restorationId:mainSplitViewController"
func makeNSViewController(context: Context) -> NSViewController {
let controller = NSSplitViewController()
controller.splitView.dividerStyle = .thin
controller.splitView.autosaveName = NSSplitView.AutosaveName(splitViewResorationIdentifier)
controller.splitView.identifier = NSUserInterfaceItemIdentifier(rawValue: splitViewResorationIdentifier)
let vcLeft = viewControllers[0]
let vcRight = viewControllers[1]
vcLeft.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
vcRight.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 70).isActive = true
let sidebarItem = NSSplitViewItem(contentListWithViewController: vcLeft)
sidebarItem.canCollapse = false
// I'm not sure if this has any impact
// controller.view.frame = CGRect(origin: .zero, size: CGSize(width: 800, height: 800))
controller.addSplitViewItem(sidebarItem)
let mainItem = NSSplitViewItem(viewController: vcRight)
controller.addSplitViewItem(mainItem)
return controller
}
func updateNSViewController(_ nsViewController: NSViewController, context: Context) {
print("should update splitView")
}
}
Yes, in such case EnvironmentObject is not injected automatically. The solution would be to separate content into designated views (for better design) and inject environment object manually.
Here it is
Text(self.env.value)
GeometryReader { geometry in
SplitView(master: {
MasterView().environmentObject(self.env)
}, detail: {
HStack {
DetailView().environmentObject(self.env)
}).frame(width: geometry.size.width, height: geometry.size.height)
}
and views
struct MasterView: View {
var body: some View {
Text("master")
.background(Color.yellow)
}
}
struct DetailView: View {
var body: some View {
HStack {
Text(self.env.value) }
.background(Color.orange)
}
}

SwiftUI - Hiding a ScrollView's indicators makes it stop scrolling

I'm trying to hide the indicators of a ScrollView but when I try doing so, the ScrollView just doesn't scroll anymore. I'm using macOS if that matters.
ScrollView(showsIndicators: false) {
// Everything is in here
}
On request of #SoOverIt
Demo:
Nothing special, just launched some other test example. Xcode 11.2 / macOS 10.15
var body : some View {
VStack {
ScrollView([.vertical], showsIndicators: false) {
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
}
.frame(height: 100)
.border(Color.blue)
}
.border(Color.red)
}
I fixed the issue.
extension View {
func hideIndicators() -> some View {
return PanelScrollView{ self }
}
}
struct PanelScrollView<Content> : View where Content : View {
let content: () -> Content
var body: some View {
PanelScrollViewControllerRepresentable(content: self.content())
}
}
struct PanelScrollViewControllerRepresentable<Content>: NSViewControllerRepresentable where Content: View{
func makeNSViewController(context: Context) -> PanelScrollViewHostingController<Content> {
return PanelScrollViewHostingController(rootView: self.content)
}
func updateNSViewController(_ nsViewController: PanelScrollViewHostingController<Content>, context: Context) {
}
typealias NSViewControllerType = PanelScrollViewHostingController<Content>
let content: Content
}
class PanelScrollViewHostingController<Content>: NSHostingController<Content> where Content : View {
var scrollView: NSScrollView?
override func viewDidAppear() {
self.scrollView = findNSScrollView(view: self.view)
self.scrollView?.scrollerStyle = .overlay
self.scrollView?.hasVerticalScroller = false
self.scrollView?.hasHorizontalScroller = false
super.viewDidAppear()
}
func findNSScrollView(view: NSView?) -> NSScrollView? {
if view?.isKind(of: NSScrollView.self) ?? false {
return (view as? NSScrollView)
}
for v in view?.subviews ?? [] {
if let vc = findNSScrollView(view: v) {
return vc
}
}
return nil
}
}
Preview:
struct MyScrollView_Previews: PreviewProvider {
static var previews: some View {
ScrollView{
VStack{
Text("hello")
Text("hello")
Text("hello")
Text("hello")
Text("hello")
}
}.hideIndicators()
}
}
So... I think that's the only way for now.
You basically just put a View over your ScrollView indicator with the same backgroundColor as your background View
Note: This obviously only works if your background is static with no content at the trailing edge.
Idea
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme: ColorScheme
let yourBackgroundColorLight: Color = .white
let yourBackgroundColorDark: Color = .black
var yourBackgroundColor: Color { colorScheme == .light ? yourBackgroundColorLight : yourBackgroundColorDark }
var body: some View {
ScrollView {
VStack {
ForEach(0..<1000) { i in
Text(String(i)).frame(width: 280).foregroundColor(.green)
}
}
}
.background(yourBackgroundColor) //<-- Same
.overlay(
HStack {
Spacer()
Rectangle()
.frame(width: 10)
.foregroundColor(yourBackgroundColor) //<-- Same
}
)
}
}
Compact version
You could improve this like that, I suppose you have your color dynamically set up inside assets.
Usage:
ScrollView {
...
}
.hideIndicators(with: <#Your Color#>)
Implementation:
extension View {
func hideIndicators(with color: Color) -> some View {
return modifier(HideIndicators(color: color))
}
}
struct HideIndicators: ViewModifier {
let color: Color
func body(content: Content) -> some View {
content
.overlay(
HStack {
Spacer()
Rectangle()
.frame(width: 10)
.foregroundColor(color)
}
)
}
}

How to convert a View (not UIView) to an image?

Similar to this thread: How to convert a UIView to an image.
I would like to convert a SwiftUI View rather than a UIView to an image.
Although SwiftUI does not provide a direct method to convert a view into an image, you still can do it. It is a little bit of a hack, but it works just fine.
In the example below, the code captures the image of two VStacks whenever they are tapped. Their contents are converted into a UIImage (that you can later save to a file if you need). In this case, I am just displaying it below.
Note that the code can be improved, but it provides the basics to get you started. I use GeometryReader to get the coordinates of the VStack to capture, but it could be improved with Preferences to make it more robust. Check the links provided, if you need to learn more about it.
Also, in order to convert an area of the screen to an image, we do need a UIView. The code uses UIApplication.shared.windows[0].rootViewController.view to get the top view, but depending on your scenario you may need to get it from somewhere else.
Good luck!
And this is the code (tested on iPhone Xr simulator, Xcode 11 beta 4):
import SwiftUI
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
struct ContentView: View {
#State private var rect1: CGRect = .zero
#State private var rect2: CGRect = .zero
#State private var uiimage: UIImage? = nil
var body: some View {
VStack {
HStack {
VStack {
Text("LEFT")
Text("VIEW")
}
.padding(20)
.background(Color.green)
.border(Color.blue, width: 5)
.background(RectGetter(rect: $rect1))
.onTapGesture { self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect1) }
VStack {
Text("RIGHT")
Text("VIEW")
}
.padding(40)
.background(Color.yellow)
.border(Color.green, width: 5)
.background(RectGetter(rect: $rect2))
.onTapGesture { self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect2) }
}
if uiimage != nil {
VStack {
Text("Captured Image")
Image(uiImage: self.uiimage!).padding(20).border(Color.black)
}.padding(20)
}
}
}
}
struct RectGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { proxy in
self.createView(proxy: proxy)
}
}
func createView(proxy: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.rect = proxy.frame(in: .global)
}
return Rectangle().fill(Color.clear)
}
}
Solution
Here is a possible solution that uses a UIHostingController that is inserted in the background of the rootViewController:
func convertViewToData<V>(view: V, size: CGSize, completion: #escaping (Data?) -> Void) where V: View {
guard let rootVC = UIApplication.shared.windows.first?.rootViewController else {
completion(nil)
return
}
let imageVC = UIHostingController(rootView: view.edgesIgnoringSafeArea(.all))
imageVC.view.frame = CGRect(origin: .zero, size: size)
DispatchQueue.main.async {
rootVC.view.insertSubview(imageVC.view, at: 0)
let uiImage = imageVC.view.asImage(size: size)
imageVC.view.removeFromSuperview()
completion(uiImage.pngData())
}
}
You also need a modified version of the asImage extension proposed here by kontiki (setting UIGraphicsImageRendererFormat is necessary as new devices can have 2x or 3x scale):
extension UIView {
func asImage(size: CGSize) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
return UIGraphicsImageRenderer(size: size, format: format).image { context in
layer.render(in: context.cgContext)
}
}
}
Usage
Assuming you have some test view:
var testView: some View {
ZStack {
Color.blue
Circle()
.fill(Color.red)
}
}
you can convert this View to Data which can be used to return an Image (or UIImage):
convertViewToData(view: testView, size: CGSize(width: 300, height: 300)) {
guard let imageData = $0, let uiImage = UIImage(data: imageData) else { return }
return Image(uiImage: uiImage)
}
The Data object can also be saved to file, shared...
Demo
struct ContentView: View {
#State var imageData: Data?
var body: some View {
VStack {
testView
.frame(width: 50, height: 50)
if let imageData = imageData, let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
}
}
.onAppear {
convertViewToData(view: testView, size: .init(width: 300, height: 300)) {
imageData = $0
}
}
}
var testView: some View {
ZStack {
Color.blue
Circle()
.fill(Color.red)
}
}
}
Following kontiki answer, here is the Preferences way
import SwiftUI
struct ContentView: View {
#State private var uiImage: UIImage? = nil
#State private var rect1: CGRect = .zero
#State private var rect2: CGRect = .zero
var body: some View {
VStack {
HStack {
VStack {
Text("LEFT")
Text("VIEW")
}
.padding(20)
.background(Color.green)
.border(Color.blue, width: 5)
.getRect($rect1)
.onTapGesture {
self.uiImage = self.rect1.uiImage
}
VStack {
Text("RIGHT")
Text("VIEW")
}
.padding(40)
.background(Color.yellow)
.border(Color.green, width: 5)
.getRect($rect2)
.onTapGesture {
self.uiImage = self.rect2.uiImage
}
}
if uiImage != nil {
VStack {
Text("Captured Image")
Image(uiImage: self.uiImage!).padding(20).border(Color.black)
}.padding(20)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension CGRect {
var uiImage: UIImage? {
UIApplication.shared.windows
.filter{ $0.isKeyWindow }
.first?.rootViewController?.view
.asImage(rect: self)
}
}
extension View {
func getRect(_ rect: Binding<CGRect>) -> some View {
self.modifier(GetRect(rect: rect))
}
}
struct GetRect: ViewModifier {
#Binding var rect: CGRect
var measureRect: some View {
GeometryReader { proxy in
Rectangle().fill(Color.clear)
.preference(key: RectPreferenceKey.self, value: proxy.frame(in: .global))
}
}
func body(content: Content) -> some View {
content
.background(measureRect)
.onPreferenceChange(RectPreferenceKey.self) { (rect) in
if let rect = rect {
self.rect = rect
}
}
}
}
extension GetRect {
struct RectPreferenceKey: PreferenceKey {
static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) {
value = nextValue()
}
typealias Value = CGRect?
static var defaultValue: CGRect? = nil
}
}
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
I came up with a solution when you can save to UIImage a SwiftUI View that is not on the screen.
The solution looks a bit weird, but works fine.
First create a class that serves as connection between UIHostingController and your SwiftUI. In this class, define a function that you can call to copy your "View's" image. After you do this, simply "Publish" new value to update your views.
class Controller:ObservableObject {
#Published var update=false
var img:UIImage?
var hostingController:MySwiftUIViewHostingController?
init() {
}
func copyImage() {
img=hostingController?.copyImage()
update=true
}
}
Then wrap your SwiftUI View that you want to copy via UIHostingController
class MySwiftUIViewHostingController: UIHostingController<TestView> {
override func viewDidLoad() {
super.viewDidLoad()
}
func copyImage()->UIImage {
let renderer = UIGraphicsImageRenderer(bounds: self.view.bounds)
return renderer.image(actions: { (c) in
self.view.layer.render(in: c.cgContext)
})
}
}
The copyImage() function returns the controller's view as UIImage
Now you need to present UIHostingController:
struct MyUIViewController:UIViewControllerRepresentable {
#ObservedObject var cntrl:Controller
func makeUIViewController(context: Context) -> MySwiftUIViewHostingController {
let controller=MySwiftUIViewHostingController(rootView: TestView())
cntrl.hostingController=controller
return controller
}
func updateUIViewController(_ uiViewController: MySwiftUIViewHostingController, context: Context) {
}
}
And the rest as follows:
struct TestView:View {
var body: some View {
VStack {
Text("Title")
Image("img2")
.resizable()
.aspectRatio(contentMode: .fill)
Text("foot note")
}
}
}
import SwiftUI
struct ContentView: View {
#ObservedObject var cntrl=Controller()
var body: some View {
ScrollView {
VStack {
HStack {
Image("img1")
.resizable()
.scaledToFit()
.border(Color.black, width: 2.0)
.onTapGesture(count: 2) {
print("tap registered")
self.cntrl.copyImage()
}
Image("img1")
.resizable()
.scaledToFit()
.border(Color.black, width: 2.0)
}
TextView()
ImageCopy(cntrl: cntrl)
.border(Color.red, width: 2.0)
TextView()
TextView()
TextView()
TextView()
TextView()
MyUIViewController(cntrl: cntrl)
.aspectRatio(contentMode: .fit)
}
}
}
}
struct ImageCopy:View {
#ObservedObject var cntrl:Controller
var body: some View {
VStack {
Image(uiImage: cntrl.img ?? UIImage())
.resizable()
.frame(width: 200, height: 200, alignment: .center)
}
}
}
struct TextView:View {
var body: some View {
VStack {
Text("Bla Bla Bla Bla Bla ")
Text("Bla Bla Bla Bla Bla ")
Text("Bla Bla Bla Bla Bla ")
Text("Bla Bla Bla Bla Bla ")
Text("Bla Bla Bla Bla Bla ")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You need img1 and img2 (the one that gets copied). I put everything into a scrollview so that one can see that the image copies fine even when not on the screen.