SwiftUI UIScrollView content can be scrolled out of frame - swiftui

I implemented a UIScrollView from UIKit in SwiftUI (without storyboard) because there is nothing similar in SwiftUI. It works quite well so far, but the ScrollView is not limited by the size of the content, but you can still scroll over the edge of the content. The stronger the zoom factor, the stronger this effect becomes apparent.
UIScrollView Wrapper in SwiftUI:
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
#Binding var didZoom: Bool
private var content: Content
init(didZoom: Binding<Bool>, #ViewBuilder content: () -> Content) {
_didZoom = didZoom
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .black
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content), didZoom: $didZoom)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
#Binding var didZoom: Bool
init(hostingController: UIHostingController<Content>, didZoom: Binding<Bool>) {
self.hostingController = hostingController
_didZoom = didZoom
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
didZoom = !(scrollView.zoomScale == scrollView.minimumZoomScale)
}
}
}
SwiftUI ContentView:
struct ContentView: View {
var body: some View {
ZoomableScrollView {
Rectangle()
.frame(width: 420, height: 600)
}
}
}

You're making whole screen zoomable if you want to make only rectangle zoomable then only wrap that rectangle within zoomablescrollview
struct ContentView: View {
var body: some View {
ZStack{
ZoomableScrollView {
Rectangle()
.frame(width: 420, height: 600)
}.frame(width: 420,height: 600)
}
}
}

Related

How to scroll to position UIScrollView in Wrapper for SwiftUI?

i have a ScrollView from UIKit and use it for SwiftUI: Is there any way to make a paged ScrollView in SwiftUI?
Question: How can I scroll in the UIScrollView to a position with a button click on a button in a SwiftUI View OR what is also good for my needs to scroll to a position when first displaying the ScrollView
I tried contentOffset but this didnt work. Perhaps I've done something wrong.
ScrollViewWrapper:
class UIScrollViewViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = false
v.alwaysBounceVertical = true
return v
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIScrollViewViewController {
let vc = UIScrollViewViewController()
vc.hostingController.rootView = AnyView(self.content())
return vc
}
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
}
}
SwiftUI usage:
struct ContentView: View{
#ObservedObject var search = SearchBar()
var body: some View{
NavigationView{
GeometryReader{geo in
UIScrollViewWrapper{ //<-----------------
VStack{
ForEach(0..<10){i in
Text("lskdfj")
}
}
.frame(width: geo.size.width)
}
.navigationBarTitle("Test")
}
}
}
}
We will first declare the offset property in the UIViewControllerRepresentable, with the propertyWrapper #Binding, because its value can be changed by the scrollview or by the parent view (the ContentView).
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
#Binding var offset: CGPoint
init(offset: Binding<CGPoint>, #ViewBuilder content: #escaping () -> Content) {
self.content = content
_offset = offset
}
// ....//
}
If the offset changes cause of the parent view, we must apply these changes to the scrollView in the updateUIViewController function (which is called when the state of the view changes) :
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(content())
viewController.scrollView.contentOffset = offset
}
When the offset changes because the user scrolls, we must reflect this change on our Binding. To do this we must declare a Coordinator, which will be a UIScrollViewDelegate, and modify the offset in its scrollViewDidScroll function :
class Controller: NSObject, UIScrollViewDelegate {
var parent: UIScrollViewWrapper<Content>
init(parent: UIScrollViewWrapper<Content>) {
self.parent = parent
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
parent.offset = scrollView.contentOffset
}
}
and, in struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable
func makeCoordinator() -> Controller {
return Controller(parent: self)
}
Finally, for the initial offset (this is important otherwise your starting offset will always be 0), this happens in the makeUIViewController:
you have to add these lines:
vc.view.layoutIfNeeded ()
vc.scrollView.contentOffset = offset
The final project :
import SwiftUI
struct ContentView: View {
#State private var offset: CGPoint = CGPoint(x: 0, y: 200)
let texts: [String] = (1...100).map {_ in String.random(length: Int.random(in: 6...20))}
var body: some View {
ZStack(alignment: .top) {
GeometryReader { geo in
UIScrollViewWrapper(offset: $offset) { //
VStack {
Text("Start")
.foregroundColor(.red)
ForEach(texts, id: \.self) { text in
Text(text)
}
}
.padding(.top, 40)
.frame(width: geo.size.width)
}
.navigationBarTitle("Test")
}
HStack {
Text(offset.debugDescription)
Button("add") {
offset.y += 100
}
}
.padding(.bottom, 10)
.frame(maxWidth: .infinity)
.background(Color.white)
}
}
}
class UIScrollViewViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = false
v.alwaysBounceVertical = true
return v
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
pinEdges(of: scrollView, to: view)
hostingController.willMove(toParent: self)
scrollView.addSubview(hostingController.view)
pinEdges(of: hostingController.view, to: scrollView)
hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
#Binding var offset: CGPoint
init(offset: Binding<CGPoint>, #ViewBuilder content: #escaping () -> Content) {
self.content = content
_offset = offset
}
func makeCoordinator() -> Controller {
return Controller(parent: self)
}
func makeUIViewController(context: Context) -> UIScrollViewViewController {
let vc = UIScrollViewViewController()
vc.scrollView.contentInsetAdjustmentBehavior = .never
vc.hostingController.rootView = AnyView(content())
vc.view.layoutIfNeeded()
vc.scrollView.contentOffset = offset
vc.scrollView.delegate = context.coordinator
return vc
}
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(content())
viewController.scrollView.contentOffset = offset
}
class Controller: NSObject, UIScrollViewDelegate {
var parent: UIScrollViewWrapper<Content>
init(parent: UIScrollViewWrapper<Content>) {
self.parent = parent
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
parent.offset = scrollView.contentOffset
}
}
}
You will need to pass a #Binding var offset: CGPoint into the UIScrollViewWrapper then when the button is clicked in your SwiftUI view, you can update the binding value which can then be used in the update method for UIViewControllerRepresentable. Another idea is to use UIViewRepresentable instead and use that with UIScrollView. Here is a helpful article doing that and setting its offset: https://www.fivestars.blog/articles/scrollview-offset/.

Simultaneous Gestures not working for a ZoomableScrollView in SwiftUI

Using Swift5.3.2, iOS14.4.1, Xcode12.4,
I am trying to use the .simultaneousGesture modifier in SwiftUI.
As far as I understood, this modifier should make sure that gestures (such as tap, longpress, magnification etc) should be able to co-exist within a View.
In my example I am using a ZoomableScrollView. And it works fine as long as I do not use the simultaneousGesture.
But as soon as I use the extra simultaneousGesture, the ZoomableScrollView is no longer "zoomable" (i.e. none of its gestures work anymore).
What can I do to make the zoom still work AND get an extra dragGesture ?
import SwiftUI
struct MediaTabView: View {
#GestureState private var dragOffset: CGFloat = -100
var body: some View {
ZoomableScrollView {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.simultaneousGesture(
DragGesture()
.updating($dragOffset) { (value, gestureState, transaction) in
let delta = value.location.x - value.startLocation.x
if delta > 10 { // << some appropriate horizontal threshold here
gestureState = delta
print(delta)
}
}
.onEnded {
if $0.translation.width > 100 {
// Go to the previous slide
print("on ended")
}
}
)
}
}
The code for the ZoomableScrollView is here :
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .black
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}

How to create synced ScrollViews in SwiftUI

I am trying to create two synced ScrollViews in SwiftUI such that scrolling in one will result in the same scrolling in the other.
I am using the ScrollViewOffset class shown at the bottom for getting a scrollView offset value but having trouble figuring out how to scroll the other view.
I seem to be able to 'hack' it by preventing scrolling in one view and setting the content position() on the other - is there any way to actually scroll the scrollView content to a position - I know ScrollViewReader seems to allow scrolling to display content items but I can't seem to find anything that will scroll the contents to an offset position.
The problem with using position() is that it does not actually change the ScrollViews scroller positions - there seems to be no ScrollView.scrollContentsTo(point: CGPoint).
#State private var scrollOffset1: CGPoint = .zero
HStack {
ScrollViewOffset(onOffsetChange: { offset in
scrollOffset1 = offset
print("New ScrollView1 offset: \(offset)")
}, content: {
VStack {
ImageView(filteredImageProvider: self.provider)
.frame(width: imageWidth, height: imageHeight)
}
.frame(width: imageWidth + (geometry.size.width - 20) * 2, height: imageHeight + (geometry.size.height - 20) * 2)
.border(Color.white)
.id(0)
})
ScrollView([]) {
VStack {
ImageView(filteredImageProvider: self.provider, showEdits: false)
.frame(width: imageWidth, height: imageHeight)
}
.frame(width: imageWidth + (geometry.size.width - 20) * 2, height: imageHeight + (geometry.size.height - 20) * 2)
.border(Color.white)
.id(0)
.position(x: scrollOffset1.x, y: scrollOffset1.y + (imageHeight + (geometry.size.height - 20) * 2)/2)
}
}
//
// ScrollViewOffset.swift
// ZoomView
//
//
import Foundation
import SwiftUI
struct ScrollViewOffset<Content: View>: View {
let onOffsetChange: (CGPoint) -> Void
let content: () -> Content
init(
onOffsetChange: #escaping (CGPoint) -> Void,
#ViewBuilder content: #escaping () -> Content
) {
self.onOffsetChange = onOffsetChange
self.content = content
}
var body: some View {
ScrollView([.horizontal, .vertical]) {
offsetReader
content()
.padding(.top, -8)
}
.coordinateSpace(name: "frameLayer")
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: onOffsetChange)
}
var offsetReader: some View {
GeometryReader { proxy in
Color.clear
.preference(
key: ScrollOffsetPreferenceKey.self,
value: proxy.frame(in: .named("frameLayer")).origin
)
}
.frame(width: 0, height: 0)
}
}
private struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
}
Synced scroll views.
In this example, you can scroll the LHS scrollview and the RHS scrollview will be synchronised to the same position. In this example, the scrollview on the RHS is disabled, and the position is simply synchronised by using an offset.
But using the same logic and code, you can make both the LHS and RHS scrollviews synced when either of them are scrolled.
import SwiftUI
struct ContentView: View {
#State private var offset = CGFloat.zero
var body: some View {
HStack(alignment: .top) {
// MainScrollView
ScrollView {
VStack {
ForEach(0..<100) { i in
Text("Item \(i)").padding()
}
}
.background( GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) { value in
print("offset >> \(value)")
offset = value
}
}
.coordinateSpace(name: "scroll")
// Synchronised with ScrollView above
ScrollView {
VStack {
ForEach(0..<100) { i in
Text("Item \(i)").padding()
}
}
.offset(y: -offset)
}
.disabled(true)
}
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
With the current API of ScrollView, this is not possible. While you can get the contentOffset of the scrollView using methods that are widely available on the internet, the ScrollViewReader that is used to programmatically scroll a ScrollView only allows you to scroll to specific views, instead of to a contentOffset.
To achieve this functionality, you are going to have to wrap UIScrollView. Here is an implementation, although it isn't 100% stable, and is missing a good amount of scrollView functionality:
import SwiftUI
import UIKit
public struct ScrollableView<Content: View>: UIViewControllerRepresentable {
#Binding var offset: CGPoint
var content: () -> Content
public init(_ offset: Binding<CGPoint>, #ViewBuilder content: #escaping () -> Content) {
self._offset = offset
self.content = content
}
public func makeUIViewController(context: Context) -> UIScrollViewViewController {
let vc = UIScrollViewViewController()
vc.hostingController.rootView = AnyView(self.content())
vc.scrollView.setContentOffset(offset, animated: false)
vc.delegate = context.coordinator
return vc
}
public func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
// Allow for deaceleration to be done by the scrollView
if !viewController.scrollView.isDecelerating {
viewController.scrollView.setContentOffset(offset, animated: false)
}
}
public func makeCoordinator() -> Coordinator {
Coordinator(contentOffset: _offset)
}
public class Coordinator: NSObject, UIScrollViewDelegate {
let contentOffset: Binding<CGPoint>
init(contentOffset: Binding<CGPoint>) {
self.contentOffset = contentOffset
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
contentOffset.wrappedValue = scrollView.contentOffset
}
}
}
public class UIScrollViewViewController: UIViewController {
lazy var scrollView: UIScrollView = UIScrollView()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
weak var delegate: UIScrollViewDelegate?
public override func viewDidLoad() {
super.viewDidLoad()
self.scrollView.delegate = delegate
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}
struct ScrollableView_Previews: PreviewProvider {
static var previews: some View {
Wrapper()
}
struct Wrapper: View {
#State var offset: CGPoint = .init(x: 0, y: 50)
var body: some View {
HStack {
ScrollableView($offset, content: {
ForEach(0...100, id: \.self) { id in
Text("\(id)")
}
})
ScrollableView($offset, content: {
ForEach(0...100, id: \.self) { id in
Text("\(id)")
}
})
VStack {
Text("x: \(offset.x) y: \(offset.y)")
Button("Top", action: {
offset = .zero
})
.buttonStyle(.borderedProminent)
}
.frame(width: 200)
.padding()
}
}
}
}

How to save a finished image from a SwiftUI 2.0 scroll view after it's been resized and repositioned by the user

Using Swift 2.0 I am hoping to find a way to capture the resized image after the user has selected how they want to see it in the frame from the scroll view (ZoomScrollView).
I know there are complex examples out there from Swift but was hoping to find a simpler way to capture this in Swift 2.0. In all my searching I've heard references to using ZStack and some masks or overlays but can't find a simple good example.
I am hoping someone can update my example with the ZStack, masks, etc and how to extract the image for saving or provide a better example.
import SwiftUI
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#State var isAccepted: Bool = false
#State var isShowingImagePicker = false
#State var isShowingActionPicker = false
#State var sourceType:UIImagePickerController.SourceType = .camera
#State var image:UIImage?
var body: some View {
HStack {
Color(UIColor.systemYellow).frame(width: 8)
VStack(alignment: .leading) {
HStack {
Spacer()
VStack {
if image != nil {
ZoomScrollView {
Image(uiImage: image!)
.resizable()
.scaledToFit()
}
.frame(width: 300, height: 300, alignment: .center)
.clipShape(Circle())
} else {
Image(systemName: "person.crop.circle")
.resizable()
.font(.system(size: 32, weight: .light))
.frame(width: 300, height: 300, alignment: .center)
.cornerRadius(180)
.foregroundColor(Color(.systemGray))
.clipShape(Circle())
}
}
Spacer()
}
Spacer()
HStack {
Button(action: {
self.isShowingActionPicker = true
}, label: {
Text("Select Image")
.foregroundColor(.blue)
})
.frame(width: 130)
.actionSheet(isPresented: $isShowingActionPicker, content: {
ActionSheet(title: Text("Select a profile avatar picture"), message: nil, buttons: [
.default(Text("Camera"), action: {
self.isShowingImagePicker = true
self.sourceType = .camera
}),
.default(Text("Photo Library"), action: {
self.isShowingImagePicker = true
self.sourceType = .photoLibrary
}),
.cancel()
])
})
.sheet(isPresented: $isShowingImagePicker) {
imagePicker(image: $image, isShowingImagePicker: $isShowingImagePicker ,sourceType: self.sourceType)
}
Spacer()
// Save button
Button(action: {
// Save Image here... print for now just see if file dimensions are the right size
print("saved: ", image!)
self.presentationMode.wrappedValue.dismiss()
}
) {
HStack {
Text("Save").foregroundColor(isAccepted ? .gray : .blue)
}
}
.frame(width: 102)
.padding(.top)
.padding(.bottom)
//.buttonStyle(RoundedCorners())
.disabled(isAccepted) // Disable if if already isAccepted is true
}
}
Spacer()
Color(UIColor.systemYellow).frame(width: 8)
}
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top)
.background(Color(UIColor.systemYellow))
}
}
struct ZoomScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}
struct imagePicker:UIViewControllerRepresentable {
#Binding var image: UIImage?
#Binding var isShowingImagePicker: Bool
typealias UIViewControllerType = UIImagePickerController
typealias Coordinator = imagePickerCoordinator
var sourceType:UIImagePickerController.SourceType = .camera
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> imagePickerCoordinator {
return imagePickerCoordinator(image: $image, isShowingImagePicker: $isShowingImagePicker)
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
}
class imagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var image: UIImage?
#Binding var isShowingImagePicker: Bool
init(image:Binding<UIImage?>, isShowingImagePicker: Binding<Bool>) {
_image = image
_isShowingImagePicker = isShowingImagePicker
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiimage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
image = uiimage
isShowingImagePicker = false
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShowingImagePicker = false
}
}
Just want to return the image that's zoomed in the circle. The image can be square (re: the 300x300 frame), that's fine just need the zoomed image not whole screen or the original image.
the following changes were successful based the comments:
Add the following State variables:
#State private var rect: CGRect = .zero
#State private var uiimage: UIImage? = nil // resized image
Added "RectGetter" to the picked image frame after image selected selected
if image != nil {
ZoomScrollView {
Image(uiImage: image!)
.resizable()
.scaledToFit()
}
.frame(width: 300, height: 300, alignment: .center)
.clipShape(Circle())
.background(RectGetter(rect: $rect))
Here is the struct and extension I added
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
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)
}
}
Last I set the image to save
self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect)
This assumes the root controller. However, in my production app I had to point to self
self.uiimage = UIApplication.shared.windows[0].self.asImage(rect: self.rect)
Then I was able to save that image.
A couple of notes. The image returned is the rectangle which is fine. However due to the way the image is captured the rest of the rectangle outside the cropShape of a circle has the background color. In this case yellow at the for corners outside the circle. There is probably a way to have some sort of ZOrder mask that overlays the image for display when you are resizing the image but then this accesses the right layer and saves the full rectangle picture. If anyone wants to suggest further that would be a cleaner solution but this works assuming you will always display the picture in the same crop shape it was saved in.

Toolbar accessory added to UITextView as a UIViewRepresentable only displays after first launch of iMessage extension application

Would like to have the toolbar show all the time.
Any help is greatly appreciated as this is a real drag for the user experience.
I've added a toolbar to the keyboard for the TextView as shown below.
However the toolbar only shows after the app has run once. Meaning the toolbar does not show the first time the app is run. the app works every time after the initial load.
This is on IOS 14.3, Xcode 12.3, Swift 5, iMessage extension app. Fails on simulator or real device.
struct CustomTextEditor: UIViewRepresentable {
#Binding var text: String
private var returnType: UIReturnKeyType
private var keyType: UIKeyboardType
private var displayDoneBar: Bool
private var commitHandler: (()->Void)?
init(text: Binding<String>,
returnType: UIReturnKeyType = .done,
keyboardType: UIKeyboardType,
displayDoneBar: Bool,
onCommit: (()->Void)?) {
self._text = text
self.returnType = returnType
self.keyType = keyboardType
self.displayDoneBar = displayDoneBar
self.commitHandler = onCommit
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.keyboardType = keyType
textView.returnKeyType = returnType
textView.backgroundColor = .clear
textView.font = UIFont.systemFont(ofSize: 20, weight: .regular)
textView.isEditable = true
textView.delegate = context.coordinator
if self.displayDoneBar {
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
target: self,
action: nil)
let doneButton = UIBarButtonItem(title: "Close Keyboard",
style: .done,
target: self,
action: #selector(textView.doneButtonPressed(button:)))
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
toolBar.items = [flexibleSpace, doneButton, flexibleSpace]
toolBar.setItems([flexibleSpace, doneButton, flexibleSpace], animated: true)
toolBar.sizeToFit()
textView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
textView.translatesAutoresizingMaskIntoConstraints = true
textView.inputAccessoryView = toolBar
}
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: CustomTextEditor
init(_ textView: CustomTextEditor) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
}
func textViewDidEndEditing(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
parent.commitHandler?()
}
}
}
extension UITextView {
#objc func doneButtonPressed(button:UIBarButtonItem) -> Void {
self.resignFirstResponder()
}
}
This is how it's called...
import SwiftUI
final class ContentViewHostController: UIHostingController<ContentView> {
weak var myWindow: UIWindow?
init() {
super.init(rootView: ContentView())
}
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: ContentView())
}
}
let kTextColor = Color(hex: "3E484F")
let kOverlayRadius: CGFloat = 10
let kOverlayWidth: CGFloat = 2
let kOverlayColor = kTextColor
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
Spacer()
CustomTextEditor(text: $text, returnType: .default, keyboardType: .default, displayDoneBar: true, onCommit: nil)
.foregroundColor(kTextColor)
.overlay(
RoundedRectangle(cornerRadius: kOverlayRadius)
.stroke(kOverlayColor, lineWidth: kOverlayWidth)
)
.frame(width: 200, height: 100, alignment: .center)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
From MessagesViewController...
override func willBecomeActive(with conversation: MSConversation) {
let childViewCtrl = ContentViewHostController()
childViewCtrl.view.layoutIfNeeded() // avoids snapshot warning?
if let window = self.view.window {
childViewCtrl.myWindow = window
window.rootViewController = childViewCtrl
}
}