Display UIViewRepresentable as a navigationBarItem in SwiftUI - swift

I can get a button to display in the navigationbaritems in SwiftUI no problem.
I want to display a wrapped UIKit view in the same.
This is a minimal example of a real problem. I want a 100 * 4 UIView displayed as a trailing navigationbaritems.
Here is my code:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("stack overflow test")
}
.navigationBarItems(trailing: TestView())
.navigationTitle("Navigation")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct TestView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 4))
view.backgroundColor = .red
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
Right now nothing displays in the top-right hand corner of the navigation bar. Why might this be?

Instead of setting frame inside the UIViewRepresentable, set frame in navigationBarItems ContentView
Tested in Xcode 12.3 with iOS 14.3
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("stack overflow test")
}
.navigationBarItems(trailing: TestView().frame(width: 100, height: 4)) //< === Here
.navigationTitle("Navigation")
}
}
}
struct TestView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView() //< === Remove From Here
view.backgroundColor = .red
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
Note :
From iOS 14.5 navigationBarItems is deprecated. Use ToolbarItem
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("stack overflow test")
}
.toolbar { //< === Here
ToolbarItem(placement: .navigationBarTrailing) {
TestView().frame(width: 100, height: 4)
}
}
.navigationTitle("Navigation")
}
}
}

Related

How to clean PKCanvas from strokes when I leave the screen?

I have a basic app, a Main View with links to Drawing View. When I go to the Drawing View №1 and paint something with Apple Pencil, then go back to Main View, then go back to Drawing View №1 - I still see my painting. It stayed in the memory.
Question: What is a proper way to free the memory from PKCanvas and it's strokes when leaving the view?
I know I can "remove" the drawing by assigning canvasView.drawing = PKDrawing() a new blank drawing. But does it really solve the problem of keeping junk in the memory?
Here is my bare-minimum code:
import SwiftUI
import PencilKit
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
NavigationLink(destination: DrawingView()) {
Text("Go to Drawing #1")
}
.padding()
NavigationLink(destination: DrawingView()) {
Text("Go to Drawing #2")
}
.padding()
}
}
}
}
struct DrawingView: View {
#State private var canvasView = PKCanvasView()
var body: some View {
PKCanvasViewRepresentable(canvasView: $canvasView)
.frame(width: 500, height: 500)
.border(Color.blue)
}
}
struct PKCanvasViewRepresentable: UIViewRepresentable {
#Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.tool = PKInkingTool(.pen, color: .black, width: 26)
canvasView.becomeFirstResponder()
canvasView.delegate = context.coordinator
return canvasView
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PKCanvasViewDelegate, UIPencilInteractionDelegate {
var canvas: PKCanvasViewRepresentable
init(_ canvas: PKCanvasViewRepresentable) {
self.canvas = canvas
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
print("canvasViewDrawingDidChange()")
}
}
}

`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.

Block scroll down in ScrollView - SwiftUI

How can I block scroll down and only allow scroll up in order to avoid seeing the white space over the rectangle on top when scrolling?
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ScrollView {
Rectangle()
.frame(width: geo.size.width, height: 400)
.foregroundColor(.black)
Spacer()
}
}
}
}
Update: re-tested with Xcode 13.3 / iOS 15.4
I assume you want to avoid bounces, here is possible approach (tested with Xcode 12 / iOS 14)
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ScrollView {
Rectangle()
.frame(width: geo.size.width, height: 1800)
.foregroundColor(.black)
.background(ScrollViewConfigurator {
$0?.bounces = false // << here !!
})
Spacer()
}
}
}
}
struct ScrollViewConfigurator: UIViewRepresentable {
let configure: (UIScrollView?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async {
configure(view.enclosingScrollView())
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
Note: enclosingScrollView() helper is taken from my answer in How to scroll List programmatically in SwiftUI?
Test module in project is here

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)
}
)
}
}

SwiftUI frame size

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()
}
}