Change color of SF Symbols icons - uikit

I have a MenuModel struc used to have a list of icons into a table as a menu:
var menu: [MenuModel] = [SideMenuModel(icon: (UIImage(systemName: "house.fill")?.withTintColor(.systemRed))!, title: "Home"),...]
Menu appears but icons still white. I am unable to change colors using the .withTintColor option.
I have also try using it directly on
func tableView(_ tableView: UITableView, cellForRowAt
by:
self.menu[6].icon.withTintColor(.systemRed)
without success.

You can set the image color in a few different ways...
Two options:
if let img = UIImage(systemName: "lock.icloud")?.withTintColor(.systemRed, renderingMode: .alwaysOriginal) {
imgView.image = img
}
or:
if let img = UIImage(systemName: "lock.icloud") {
imgView.image = img
}
imgView.tintColor = .systemRed
Here is some example code... using the first method to create a Red image, and the second method to create a Green image:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let imgViewA = UIImageView()
let imgViewB = UIImageView()
if let img = UIImage(systemName: "lock.icloud")?.withTintColor(.systemRed, renderingMode: .alwaysOriginal) {
imgViewA.image = img
}
if let img = UIImage(systemName: "lock.icloud") {
imgViewB.image = img
}
imgViewB.tintColor = .systemGreen
imgViewA.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imgViewA)
imgViewB.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imgViewB)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgViewA.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
imgViewA.centerXAnchor.constraint(equalTo: g.centerXAnchor, constant: 0.0),
imgViewA.heightAnchor.constraint(equalToConstant: 80.0),
imgViewA.widthAnchor.constraint(equalTo: imgViewA.heightAnchor),
imgViewB.topAnchor.constraint(equalTo: imgViewA.bottomAnchor, constant: 20.0),
imgViewB.centerXAnchor.constraint(equalTo: g.centerXAnchor, constant: 0.0),
imgViewB.heightAnchor.constraint(equalToConstant: 80.0),
imgViewB.widthAnchor.constraint(equalTo: imgViewB.heightAnchor),
])
}
}
Result:

Related

How draw a rectangle hole on UIBlurEffect and move it on x axis (UIKit)

I'm trying to create blur effect on a view and than add a shape which will show image on this blurred layer (custom video editing functionality)
Currently I'm able to do it only dragging mask view from the right edge:
but when I try to do it from the left edge, I get such a effect:
func configureBlurView() {
let viewHeight: CGFloat = 60
let padding: CGFloat = 10
blurView = UIView()
blurView.layer.cornerRadius = 10
blurView.clipsToBounds = true
blurView.isHidden = true
blurView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurView)
addConstraints([
blurView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding),
blurView.bottomAnchor.constraint(equalTo: stackView.topAnchor, constant: -padding),
blurView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding),
blurView.heightAnchor.constraint(equalToConstant: viewHeight)
])
addBlurEffect(for: blurView)
}
private func addBlurEffect(for view: UIView) {
let blurEffect = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
blurEffect.alpha = 0.5
blurEffect.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(blurEffect)
addConstraints([
blurEffect.topAnchor.constraint(equalTo: view.topAnchor),
blurEffect.leadingAnchor.constraint(equalTo: view.leadingAnchor),
blurEffect.bottomAnchor.constraint(equalTo: view.bottomAnchor),
blurEffect.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
private func makeClearHole(rect: CGRect) {
let maskLayer = CAShapeLayer()
maskLayer.fillColor = UIColor.black.cgColor
let pathToOverlay = CGMutablePath()
pathToOverlay.addRect(blurView.bounds)
pathToOverlay.addRect(rect)
maskLayer.path = pathToOverlay
maskLayer.fillRule = .evenOdd
maskLayer.cornerRadius = 10
blurView.layer.mask = maskLayer
}
I'm using touchesMoved method to change orange view dimensions:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard trimmerView.isHidden == false else { return }
if let touch = touches.first{
let currentTouchPoint = touch.location(in: self)
let previousTouchPoint = touch.previousLocation(in: self)
let deltaX = currentTouchPoint.x - previousTouchPoint.x
if trimmerView.bounds.width >= 70 {
if touchStartEdge.middle {
if trimmerViewLeadingConstraint.constant < 10 {
trimmerViewLeadingConstraint.constant = 10
} else if trimmerViewTrailingConstraint.constant > -10 {
trimmerViewTrailingConstraint.constant = -10
} else {
trimmerViewLeadingConstraint.constant += deltaX
trimmerViewTrailingConstraint.constant += deltaX
}
}
if touchStartEdge.leftEdge {
if trimmerViewLeadingConstraint.constant >= 10.0 {
trimmerViewLeadingConstraint.constant += deltaX
} else if trimmerViewLeadingConstraint.constant < 10.0 {
trimmerViewLeadingConstraint.constant = 10
}
}
if touchStartEdge.rightEdge {
if trimmerViewTrailingConstraint.constant <= -10 {
trimmerViewTrailingConstraint.constant += deltaX
} else if trimmerViewTrailingConstraint.constant > -10 {
trimmerViewTrailingConstraint.constant = -10.0
}
}
}
updateProgressBarConstraints()
makeClearHole(rect: CGRect(x: 0, y: 0, width: trimmerView.frame.width, height: trimmerView.frame.height))
UIView.animate(withDuration: 0.10, delay: 0, options: .curveEaseIn) { [weak self] in
self?.layoutIfNeeded()
}
}
}
What I'd like to achieve is to remove blur effect only in bounds of orange view.
Any ideas ?? :)
Thanks for help!!
Couple ways to do this - here's one...
Add a mask to the blur effect view. As the user drags the "trimmer" update the mask.
Here's a quick example...
We'll:
create a stack view with 10 images
overlay that with a masked blur effective view
add a "draggable trimmer view"
when we drag the trimmer, we update the mask
Example View Controller
class TrimmerVC: UIViewController {
var blurView: MaskedBlurView!
let trimmerView = DragView()
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
// respect safe area when we setup constraints
let g = view.safeAreaLayoutGuide
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
stackView.heightAnchor.constraint(equalToConstant: 80.0),
])
// let's add 10 imageviews to the stack view
for i in 1...10 {
if let img = UIImage(systemName: "\(i).circle.fill") {
let imgView = UIImageView(image: img)
imgView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
stackView.addArrangedSubview(imgView)
}
}
let blurEffect = UIBlurEffect(style: .dark)
blurView = MaskedBlurView(effect: blurEffect)
blurView.alpha = 0.5
blurView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(blurView)
NSLayoutConstraint.activate([
blurView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: 0.0),
blurView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0.0),
blurView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0.0),
blurView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 0.0),
])
trimmerView.backgroundColor = .systemOrange
view.addSubview(trimmerView)
trimmerView.didDrag = { [weak self] newX in
guard let self = self else { return }
self.blurView.clearX = newX - self.stackView.frame.origin.x
}
}
// we'll use this to update the framing when the stack view width changes
// such as on device rotation
var curStackW: CGFloat = -1
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if curStackW != stackView.frame.width {
curStackW = stackView.frame.width
var r = stackView.frame
r.origin.y += r.size.height + 20.0
r.size.width = 160
r.size.height = 40
trimmerView.frame = r
blurView.clearWidth = trimmerView.frame.width
blurView.clearX = 0
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// toggle the trimmer view between
// below the stack view and
// overlaid on the stack view
if trimmerView.frame.origin.y > stackView.frame.origin.y {
let r = stackView.frame
trimmerView.frame.origin.y = r.origin.y - 6.0
trimmerView.frame.size.height = r.height + 12.0
} else {
let r = stackView.frame
trimmerView.frame.origin.y = r.origin.y + r.height + 12.0
trimmerView.frame.size.height = 60.0
}
}
}
Example Draggable "Trimmer" View
class DragView: UIView {
var didDrag: ((CGFloat) -> ())?
let maskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
maskLayer.fillColor = UIColor.red.cgColor
layer.mask = maskLayer
}
override func layoutSubviews() {
super.layoutSubviews()
let pathToOverlay = CGMutablePath()
pathToOverlay.addRect(bounds)
pathToOverlay.addRect(bounds.insetBy(dx: 20.0, dy: 8.0))
maskLayer.path = pathToOverlay
maskLayer.fillRule = .evenOdd
maskLayer.cornerRadius = 10
}
var touchStartX: CGFloat = 0
var frameStartX: CGFloat = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
touchStartX = touch.location(in: self.superview!).x
frameStartX = self.frame.origin.x
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let loc = touch.location(in: self.superview!)
self.frame.origin.x = frameStartX + (loc.x - touchStartX)
didDrag?(self.frame.origin.x)
}
}
Example Masked Blur View
class MaskedBlurView: UIVisualEffectView {
public var clearWidth: CGFloat = 100 {
didSet { updateMask() }
}
public var clearX: CGFloat = 0 {
didSet { updateMask() }
}
private let maskLayer = CAShapeLayer()
override init(effect: UIVisualEffect?) {
super.init(effect: effect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
maskLayer.fillColor = UIColor.red.cgColor
layer.mask = maskLayer
}
func updateMask() {
let leftR = CGRect(x: 0, y: 0, width: clearX, height: bounds.height)
let rightR = CGRect(x: clearX + clearWidth, y: 0, width: bounds.width, height: bounds.height)
let bez = UIBezierPath(rect: leftR)
bez.append(UIBezierPath(rect: rightR))
maskLayer.path = bez.cgPath
}
override func layoutSubviews() {
super.layoutSubviews()
maskLayer.frame = bounds
}
}
When running (in landscape orientation) it will start like this:
I placed the "trimmer" view below the stack view to make it a little more clear what's happening.
As we drag the trimmer view, the blur view's mask will be updated:
Tapping anywhere in an empty part of the screen will toggle the trimmer view between "under the stack view" and "overlaid on the stack view":
This was just put together quickly -- you should have no trouble restructuring the code to wrap everything into a single custom view (or however it would work best for your needs).

SwiftUI exporting the content of Canvas

Does anyone know how to export the content of a Canvas into an Image?
With SwiftUI, it is possible to generate an Image from a View with an extension
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
This works great for simple views like Button, but for Canvas it always generates an empty image.
For example, with the following code, the image generated by the button is fine, but the one of the Canvas is always empty.
import SwiftUI
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
struct ContentView: View {
var textView: some View {
Text("Hello, SwiftUI")
.padding()
.background(Color.green)
.foregroundColor(.white)
.clipShape(Capsule())
}
var canvas: some View {
Canvas { context, size in
var path = Path()
path.move(to: CGPoint(x: 0, y:0))
path.addLine(to: CGPoint(x: size.width/2, y:0))
path.addLine(to: CGPoint(x: size.width/2, y:size.height/2))
path.addLine(to: CGPoint(x: 0, y:size.height/2))
path.closeSubpath()
context.fill(path, with: .color(.blue))
}
}
var body: some View {
VStack {
textView
canvas
Button("Save to image: Canvas") {
if let view = canvas as? Canvas<EmptyView> {
let image = view.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
Button("Save to image: Text") {
if let view = textView as? Text {
let image = view.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
}
}
}
Apply a frame to the canvas and it should work. E.g.
canvas.frame(width: 300, height: 300)
Answer is here: Swift UI exporting content of canvas. Apply the frame in the line where you get the snapshot, i.e.
let newImage = canvasView.frame(width: 300, height: 300).snapshot()

Looping Videos with XCode ARKit Image Tracking

Hello I'm new into developing and right now I'm working on an AR application which works with image tracking and it should play different videos on different tracked images.
Right now the video stops, how can I loop the video?
Also if the image is not in the camera view, the video continues. Is there a pause function too?
I would be thankful for every hints.
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARImageTrackingConfiguration()
if let trackedImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: Bundle.main) {
configuration.trackingImages = trackedImages
configuration.maximumNumberOfTrackedImages = 2
}
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
let node = SCNNode()
if let imageAnchor = anchor as? ARImageAnchor {
let size = imageAnchor.referenceImage.physicalSize
var videoNode = SKVideoNode()
switch imageAnchor.name {
case "Image1":
videoNode = SKVideoNode(fileNamed: "Image1.mp4")
case "Image2":
videoNode = SKVideoNode(fileNamed: "Image2.mp4")
case "Image3":
videoNode = SKVideoNode(fileNamed: "Image3.mp4")
default:
break
}
videoNode.play()
let videoScene = SKScene(size: CGSize(width: 1280, height: 960))
videoScene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
videoScene.addChild(videoNode)
let plane = SCNPlane(width: size.width, height: size.height)
plane.firstMaterial?.diffuse.contents = videoScene
let planeNode = SCNNode(geometry: plane)
plane.firstMaterial?.isDoubleSided = true
planeNode.eulerAngles.x = .pi / 2
node.addChildNode(planeNode)
return node
}
return nil
}
}

How to auto-expand height of NSTextView in SwiftUI?

How do I properly implement NSView constraints on the NSTextView below so it interacts with SwiftUI .frame()?
Goal
An NSTextView that, upon new lines, expands its frame vertically to force a SwiftUI parent view to render again (i.e., expand a background panel that's under the text + push down other content in VStack). The parent view is already wrapped in a ScrollView. Since the SwiftUI TextEditor is ugly and under-featured, I'm guessing several others new to MacOS will wonder how to do the same.
Update
#Asperi pointed out a sample for UIKit buried in another thread. I tried adapting that for AppKit, but there's some loop in the async recalculateHeight function. I'll look more at it with coffee tomorrow. Thanks Asperi. (Whoever you are, you are the SwiftUI SO daddy.)
Problem
The NSTextView implementation below edits merrily, but disobeys SwiftUI's vertical frame. Horizontally all is obeyed, but texts just continues down past the vertical height limit. Except, when switching focus away, the editor crops that extra text... until editing begins again.
What I've Tried
Sooo many posts as models. Below are a few. My shortfall I think is misunderstanding how to set constraints, how to use NSTextView objects, and perhaps overthinking things.
I've tried implementing an NSTextContainer, NSLayoutManager, and NSTextStorage stack together in the code below, but no progress.
I've played with GeometryReader inputs, no dice.
I've printed LayoutManager and TextContainer variables on textdidChange(), but am not seeing dimensions change upon new lines. Also tried listening for .boundsDidChangeNotification / .frameDidChangeNotification.
GitHub: unnamedd MacEditorTextView.swift <- Removed its ScrollView, but couldn't get text constraints right after doing so
SO: Multiline editable text field in SwiftUI <- Helped me understand how to wrap, removed the ScrollView
SO: Using a calculation by layoutManager <- My implementation didn't work
Reddit: Wrap NSTextView in SwiftUI <- Tips seem spot on, but lack AppKit knowledge to follow
SO: Autogrow height with intrinsicContentSize <- My implementation didn't work
SO: Changing a ScrollView <- Couldn't figure out how to extrapolate
SO: Cocoa tutorial on setting up an NSTextView
Apple NSTextContainer Class
Apple Tracking the Size of a Text View
ContentView.swift
import SwiftUI
import Combine
struct ContentView: View {
#State var text = NSAttributedString(string: "Testing.... testing...")
let nsFont: NSFont = .systemFont(ofSize: 20)
var body: some View {
// ScrollView would go here
VStack(alignment: .center) {
GeometryReader { geometry in
NSTextEditor(text: $text.didSet { text in react(to: text) },
nsFont: nsFont,
geometry: geometry)
.frame(width: 500, // Wraps to width
height: 300) // Disregards this during editing
.background(background)
}
Text("Editing text above should push this down.")
}
}
var background: some View {
...
}
// Seeing how updates come back; I prefer setting them on textDidEndEditing to work with a database
func react(to text: NSAttributedString) {
print(#file, #line, #function, text)
}
}
// Listening device into #State
extension Binding {
func didSet(_ then: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
then($0)
self.wrappedValue = $0
}
)
}
}
NSTextEditor.swift
import SwiftUI
struct NSTextEditor: View, NSViewRepresentable {
typealias Coordinator = NSTextEditorCoordinator
typealias NSViewType = NSTextView
#Binding var text: NSAttributedString
let nsFont: NSFont
var geometry: GeometryProxy
func makeNSView(context: NSViewRepresentableContext<NSTextEditor>) -> NSTextEditor.NSViewType {
return context.coordinator.textView
}
func updateNSView(_ nsView: NSTextView, context: NSViewRepresentableContext<NSTextEditor>) { }
func makeCoordinator() -> NSTextEditorCoordinator {
let coordinator = NSTextEditorCoordinator(binding: $text,
nsFont: nsFont,
proxy: geometry)
return coordinator
}
}
class NSTextEditorCoordinator : NSObject, NSTextViewDelegate {
let textView: NSTextView
var font: NSFont
var geometry: GeometryProxy
#Binding var text: NSAttributedString
init(binding: Binding<NSAttributedString>,
nsFont: NSFont,
proxy: GeometryProxy) {
_text = binding
font = nsFont
geometry = proxy
textView = NSTextView(frame: .zero)
textView.autoresizingMask = [.height, .width]
textView.textColor = NSColor.textColor
textView.drawsBackground = false
textView.allowsUndo = true
textView.isAutomaticLinkDetectionEnabled = true
textView.displaysLinkToolTips = true
textView.isAutomaticDataDetectionEnabled = true
textView.isAutomaticTextReplacementEnabled = true
textView.isAutomaticDashSubstitutionEnabled = true
textView.isAutomaticSpellingCorrectionEnabled = true
textView.isAutomaticQuoteSubstitutionEnabled = true
textView.isAutomaticTextCompletionEnabled = true
textView.isContinuousSpellCheckingEnabled = true
textView.usesAdaptiveColorMappingForDarkAppearance = true
// textView.importsGraphics = true // 100% size, layoutManger scale didn't fix
// textView.allowsImageEditing = true // NSFileWrapper error
// textView.isIncrementalSearchingEnabled = true
// textView.usesFindBar = true
// textView.isSelectable = true
// textView.usesInspectorBar = true
// Context Menu show styles crashes
super.init()
textView.textStorage?.setAttributedString($text.wrappedValue)
textView.delegate = self
}
// Calls on every character stroke
func textDidChange(_ notification: Notification) {
switch notification.name {
case NSText.boundsDidChangeNotification:
print("bounds did change")
case NSText.frameDidChangeNotification:
print("frame did change")
case NSTextView.frameDidChangeNotification:
print("FRAME DID CHANGE")
case NSTextView.boundsDidChangeNotification:
print("BOUNDS DID CHANGE")
default:
return
}
// guard notification.name == NSText.didChangeNotification,
// let update = (notification.object as? NSTextView)?.textStorage else { return }
// text = update
}
// Calls only after focus change
func textDidEndEditing(_ notification: Notification) {
guard notification.name == NSText.didEndEditingNotification,
let update = (notification.object as? NSTextView)?.textStorage else { return }
text = update
}
}
Quick Asperi's answer from a UIKit thread
Crash
*** Assertion failure in -[NSCGSWindow setSize:], NSCGSWindow.m:1458
[General] Invalid parameter not satisfying:
size.width >= 0.0
&& size.width < (CGFloat)INT_MAX - (CGFloat)INT_MIN
&& size.height >= 0.0
&& size.height < (CGFloat)INT_MAX - (CGFloat)INT_MIN
import SwiftUI
struct AsperiMultiLineTextField: View {
private var placeholder: String
private var onCommit: (() -> Void)?
#Binding private var text: NSAttributedString
private var internalText: Binding<NSAttributedString> {
Binding<NSAttributedString>(get: { self.text } ) {
self.text = $0
self.showingPlaceholder = $0.string.isEmpty
}
}
#State private var dynamicHeight: CGFloat = 100
#State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<NSAttributedString>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.string.isEmpty)
}
var body: some View {
NSTextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit)
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
.background(placeholderView, alignment: .topLeading)
}
#ViewBuilder
var placeholderView: some View {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
fileprivate struct NSTextViewWrapper: NSViewRepresentable {
typealias NSViewType = NSTextView
#Binding var text: NSAttributedString
#Binding var calculatedHeight: CGFloat
var onDone: (() -> Void)?
func makeNSView(context: NSViewRepresentableContext<NSTextViewWrapper>) -> NSTextView {
let textField = NSTextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = NSFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.drawsBackground = false
textField.allowsUndo = true
/// Disabled these lines as not available/neeed/appropriate for AppKit
// textField.isUserInteractionEnabled = true
// textField.isScrollEnabled = false
// if nil != onDone {
// textField.returnKeyType = .done
// }
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
}
func updateNSView(_ NSView: NSTextView, context: NSViewRepresentableContext<NSTextViewWrapper>) {
NSTextViewWrapper.recalculateHeight(view: NSView, result: $calculatedHeight)
}
fileprivate static func recalculateHeight(view: NSView, result: Binding<CGFloat>) {
/// UIView.sizeThatFits is not available in AppKit. Tried substituting below, but there's a loop that crashes.
// let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
// tried reportedSize = view.frame, view.intrinsicContentSize
let reportedSize = view.fittingSize
let newSize = CGSize(width: reportedSize.width, height: CGFloat.greatestFiniteMagnitude)
if result.wrappedValue != newSize.height {
DispatchQueue.main.async {
result.wrappedValue = newSize.height // !! must be called asynchronously
}
}
}
final class Coordinator: NSObject, NSTextViewDelegate {
var text: Binding<NSAttributedString>
var calculatedHeight: Binding<CGFloat>
var onDone: (() -> Void)?
init(text: Binding<NSAttributedString>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
self.text = text
self.calculatedHeight = height
self.onDone = onDone
}
func textDidChange(_ notification: Notification) {
guard notification.name == NSText.didChangeNotification,
let textView = (notification.object as? NSTextView),
let latestText = textView.textStorage else { return }
text.wrappedValue = latestText
NSTextViewWrapper.recalculateHeight(view: textView, result: calculatedHeight)
}
func textView(_ textView: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool {
if let onDone = self.onDone, replacementString == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
}
}
Solution thanks to #Asperi's tip to convert his UIKit code in this post. A few things had to change:
NSView also lacks the view.sizeThatFits() for a proposed bounds change, so I found that the view's .visibleRect would work instead.
Bugs:
There is a bobble on first render (from smaller vertically to the proper size). I thought it was caused by the recalculateHeight(), which would print out some smaller values initially. A gating statement there stopped those values, but the bobble is still there.
Currently I set the placeholder text's inset by a magic number, which should be done based on the NSTextView's attributes, but I didn't find anything usable yet. If it has the same font I guess I could just add a space or two in front of the placeholder text and be done with it.
Hope this saves some others making SwiftUI Mac apps some time.
import SwiftUI
// Wraps the NSTextView in a frame that can interact with SwiftUI
struct MultilineTextField: View {
private var placeholder: NSAttributedString
#Binding private var text: NSAttributedString
#State private var dynamicHeight: CGFloat // MARK TODO: - Find better way to stop initial view bobble (gets bigger)
#State private var textIsEmpty: Bool
#State private var textViewInset: CGFloat = 9 // MARK TODO: - Calculate insetad of magic number
var nsFont: NSFont
init (_ placeholder: NSAttributedString = NSAttributedString(string: ""),
text: Binding<NSAttributedString>,
nsFont: NSFont) {
self.placeholder = placeholder
self._text = text
_textIsEmpty = State(wrappedValue: text.wrappedValue.string.isEmpty)
self.nsFont = nsFont
_dynamicHeight = State(initialValue: nsFont.pointSize)
}
var body: some View {
ZStack {
NSTextViewWrapper(text: $text,
dynamicHeight: $dynamicHeight,
textIsEmpty: $textIsEmpty,
textViewInset: $textViewInset,
nsFont: nsFont)
.background(placeholderView, alignment: .topLeading)
// Adaptive frame applied to this NSViewRepresentable
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
}
}
// Background placeholder text matched to default font provided to the NSViewRepresentable
var placeholderView: some View {
Text(placeholder.string)
// Convert NSFont
.font(.system(size: nsFont.pointSize))
.opacity(textIsEmpty ? 0.3 : 0)
.padding(.leading, textViewInset)
.animation(.easeInOut(duration: 0.15))
}
}
// Creates the NSTextView
fileprivate struct NSTextViewWrapper: NSViewRepresentable {
#Binding var text: NSAttributedString
#Binding var dynamicHeight: CGFloat
#Binding var textIsEmpty: Bool
// Hoping to get this from NSTextView,
// but haven't found the right parameter yet
#Binding var textViewInset: CGFloat
var nsFont: NSFont
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text,
height: $dynamicHeight,
textIsEmpty: $textIsEmpty,
nsFont: nsFont)
}
func makeNSView(context: NSViewRepresentableContext<NSTextViewWrapper>) -> NSTextView {
return context.coordinator.textView
}
func updateNSView(_ textView: NSTextView, context: NSViewRepresentableContext<NSTextViewWrapper>) {
NSTextViewWrapper.recalculateHeight(view: textView, result: $dynamicHeight, nsFont: nsFont)
}
fileprivate static func recalculateHeight(view: NSView, result: Binding<CGFloat>, nsFont: NSFont) {
// Uses visibleRect as view.sizeThatFits(CGSize())
// is not exposed in AppKit, except on NSControls.
let latestSize = view.visibleRect
if result.wrappedValue != latestSize.height &&
// MARK TODO: - The view initially renders slightly smaller than needed, then resizes.
// I thought the statement below would prevent the #State dynamicHeight, which
// sets itself AFTER this view renders, from causing it. Unfortunately that's not
// the right cause of that redawing bug.
latestSize.height > (nsFont.pointSize + 1) {
DispatchQueue.main.async {
result.wrappedValue = latestSize.height
print(#function, latestSize.height)
}
}
}
}
// Maintains the NSTextView's persistence despite redraws
fileprivate final class Coordinator: NSObject, NSTextViewDelegate, NSControlTextEditingDelegate {
var textView: NSTextView
#Binding var text: NSAttributedString
#Binding var dynamicHeight: CGFloat
#Binding var textIsEmpty: Bool
var nsFont: NSFont
init(text: Binding<NSAttributedString>,
height: Binding<CGFloat>,
textIsEmpty: Binding<Bool>,
nsFont: NSFont) {
_text = text
_dynamicHeight = height
_textIsEmpty = textIsEmpty
self.nsFont = nsFont
textView = NSTextView(frame: .zero)
textView.isEditable = true
textView.isSelectable = true
// Appearance
textView.usesAdaptiveColorMappingForDarkAppearance = true
textView.font = nsFont
textView.textColor = NSColor.textColor
textView.drawsBackground = false
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
// Functionality (more available)
textView.allowsUndo = true
textView.isAutomaticLinkDetectionEnabled = true
textView.displaysLinkToolTips = true
textView.isAutomaticDataDetectionEnabled = true
textView.isAutomaticTextReplacementEnabled = true
textView.isAutomaticDashSubstitutionEnabled = true
textView.isAutomaticSpellingCorrectionEnabled = true
textView.isAutomaticQuoteSubstitutionEnabled = true
textView.isAutomaticTextCompletionEnabled = true
textView.isContinuousSpellCheckingEnabled = true
super.init()
// Load data from binding and set font
textView.textStorage?.setAttributedString(text.wrappedValue)
textView.textStorage?.font = nsFont
textView.delegate = self
}
func textDidChange(_ notification: Notification) {
// Recalculate height after every input event
NSTextViewWrapper.recalculateHeight(view: textView, result: $dynamicHeight, nsFont: nsFont)
// If ever empty, trigger placeholder text visibility
if let update = (notification.object as? NSTextView)?.string {
textIsEmpty = update.isEmpty
}
}
func textDidEndEditing(_ notification: Notification) {
// Update binding only after editing ends; useful to gate NSManagedObjects
$text.wrappedValue = textView.attributedString()
}
}
I found nice gist code created by unnamedd.
https://gist.github.com/unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0
Sample Usage:
MacEditorTextView(
text: $text,
isEditable: true,
font: .monospacedSystemFont(ofSize: 12, weight: .regular)
)
.frame(minWidth: 300,
maxWidth: .infinity,
minHeight: 100,
maxHeight: .infinity)
.padding(12)
.cornerRadius(8)

NSImageView (added programmatically) doesn't show image, but shows color

Swift 4. Very simple project, all I did - just added a NSImageView programmatically, backgroundColor and NSImage from the .jpg file. I see the good pink color, but can't see the image at all! I tried many different approaches and some was successful (Image showed up well in collection view and if NSImageView was added manually in the story board) but I need in simple programmatically method. Here is all of my code:
class ViewController: NSViewController {
var image: NSImage = NSImage()
var ivTest = NSImageView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.ivTest)
self.ivTest.wantsLayer = true
self.ivTest.layer?.backgroundColor = NSColor.systemPink.cgColor
self.ivTest.layer?.frame = NSRect(x: 0, y: 0, width: 100, height: 100)
let manager = FileManager.default
var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
url = url?.appendingPathComponent("night.jpg")
image = NSImage(byReferencing: url!)
if (image.isValid == true){
print("valid")
print("image size \(image.size.width):\(image.size.height)")
self.ivTest.image = image
} else {
print("not valid")
}
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
output:
result:
thank so much...
--- edited ---
Yes, thank You! Just added this and saw image:
self.ivTest.frame = NSRect(x: 0, y: 0, width: 100, height: 100)

Resources