Store/Map Data of Custom View inside UITextView - ios

I am trying to achieve an editable textView with the ability to embed a 'Custom View' as shown:
This view encompasses 2 textfields (it is showing a word definition). The view dynamically changes dependant on what definition is selected. I have currently been able to add it to the textView as an NSAttachment as an image (since I can't attach a custom view to a textView) but if I go about it this way I have no way of accessing what the text is inside the custom view if I need to update the view.
Can anyone give me a solution where I would be able to store the info that is in the custom view somehow? I want to make sure in future I would be able to actually access the data in case the view design changes and I wanted to update it. Do I need to change to a WebView and somehow create a custom parse in HTML that looks for definition info and then formats it?
EDIT: Code used to create NSAttachment. The returnView() function returns a view object as shown in red in the photo above. The rest of the viewController is just a textView which saves the NSAttributedString to a Core Data Object.
func viewToNSAttachment() {
let view = returnView()
view.layoutIfNeeded()
// Render Image
let renderer = UIGraphicsImageRenderer(size: view.frame.size)
let image = renderer.image {
view.layer.render(in: $0.cgContext)
}
// Create Attachment
let attachment = NSTextAttachment()
attachment.image = image
attachment.bounds = CGRect(origin: .zero, size: image.size)
// Current Attributed String
let atStr = NSMutableAttributedString(attributedString: textView.attributedText)
// Insert Attachment
let attachmentAtStr = NSAttributedString(attachment: attachment)
if let selectedRange = textView.selectedTextRange {
let cursorIndex = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
atStr.insert(attachmentAtStr, at: cursorIndex)
atStr.addAttributes(self.textView.typingAttributes, range: NSRange(location: cursorIndex, length: 1))
} else {
atStr.append(attachmentAtStr)
}
textView.attributedText = atStr
}

Related

How to add a small image on right side of text whatever font size it is?

I'm trying to achieve this on iOS using Swift and storyboards:
Notice that the image size on the right never changes, only the text font size changes depending on the user's default font size. I want the image to remain at the end of the label.
Does anyone have an idea how I can do it?
The simplest is to create an extension on UILabel and append whatever image/icon to the end of the attributedText, something along these lines:
extension UILabel {
func appendIcon() {
let iconAttachment = NSTextAttachment()
iconAttachment.image = UIImage(systemName: "info.circle.fill")
let iconString = NSAttributedString(attachment: iconAttachment)
guard let attributedText = self.attributedText else { return }
let currentString = NSMutableAttributedString(attributedString: attributedText)
currentString.append(iconString)
self.attributedText = currentString
}
}
And then call that programmatically.
label.appendIcon() // will need to create outlet

UITextView rich text editing

I need to implement a text editor using UITextView that supports:
Bold/Italic/Underline
Color,Font,font size changes
Paragraph alignment
List format (bullets, numbers, etc.)
Custom selection of text anywhere in the text view and change the properties
So far I have managed to do it without NSTextStorage but it seems I am hitting limits. For instance, to change font, I use UIFontPickerViewController and change the font as follows:
func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
if let selectedFontDesc = viewController.selectedFontDescriptor {
let font = UIFont(descriptor: selectedFontDesc, size: selectedFontDesc.pointSize)
self.selectedFont = font
self.textView.typingAttributes = [NSAttributedString.Key.foregroundColor: self.selectedColor ?? UIColor.white, NSAttributedString.Key.font: self.selectedFont ?? UIFont.preferredFont(forTextStyle: .body, compatibleWith: nil)]
if let range = self.textView.selectedTextRange, let selectedFont = selectedFont {
let attributedText = NSMutableAttributedString(attributedString: self.textView.attributedText)
let location = textView.offset(from: textView.beginningOfDocument, to: range.start)
let length = textView.offset(from: range.start, to: range.end)
let nsRange = NSRange(location: location, length: length)
attributedText.setAttributes([NSAttributedString.Key.font : selectedFont], range: nsRange)
self.textView.attributedText = attributedText
}
}
}
This works but the problem is it resets the color of the selected text and other properties. I need to understand a way in which the existing attributed of the text under selection are not disturbed. I suspect the way to do is with using NSTextStorage but I can't find anything good on internet that explains the right use of NSTextStorage to achieve this.
The problem is this call:
attributedText.setAttributes...
This, as you have observed, makes the attribute you provide (here, the font) the only attribute of this range. Instead, you want to add your font attribute to the existing attributes:
https://developer.apple.com/documentation/foundation/nsmutableattributedstring/1414304-addattributes

How to convert SwiftUI Image back to UIImage? [duplicate]

since the documentation on swiftUI isn't great yet I wanted to ask how I can convert an "image" to an "UIImage" or how to convert an "image" to pngData/jpgData
let image = Image(systemName: "circle.fill")
let UIImage = image as UIImage
There is no direct way of converting an Image to UIImage. instead, you should treat the Image as a View, and try to convert that View to a UIImage.
Image conforms to View, so we already have the View we need. now we just need to convert that View to a UIImage.
We need 2 components to achieve this. First, a function to change our Image/View to a UIView, and second one, to change the UIView we created to UIImage.
For more Convenience, both functions are declared as Extensions to their appropriate types.
extension View {
// This function changes our View to UIView, then calls another function
// to convert the newly-made UIView to a UIImage.
public func asUIImage() -> UIImage {
let controller = UIHostingController(rootView: self)
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
// here is the call to the function that converts UIView to UIImage: `.asUIImage()`
let image = controller.view.asUIImage()
controller.view.removeFromSuperview()
return image
}
}
extension UIView {
// This is the function to convert UIView to UIImage
public func asUIImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
How To Use?
let image: Image = Image("MyImageName") // Create an Image anyhow you want
let uiImage: UIImage = image.asUIImage() // Works Perfectly
Bonus
As i said, we are treating the Image, as a View. In the process, we don't use any specific features of Image, the only thing that is important is that our Image is a View (conforms to View protocol).
This means that with this method, you can not only convert an Image to a UIImage, but also you can convert any View to a UIImage.
var myView: some View {
// create the view here
}
let uiImage = myView.asUIImage() // Works Perfectly
Such thing is not possible with SwiftUI, and I bet it will never be. It goes againts the whole framework concept. However, you can do:
let uiImage = UIImage(systemName: "circle.fill")
let image = Image(uiImage: uiImage)

Can you convert uiview that is not currently being displayed to uiimage

I have found numerous examples of converting UIView to UIImage, and they work perfectly for once the view has been laid out etc. Even in my view controller with many rows, some of which are net yet displaying on the screen, I can do the conversion. Unfortunately, for some of the views that are too far down in the table (and hence have not yet been "drawn"), doing the conversion produces a blank UIImage.
I've tried calling setNeedsDisplay and layoutIfNeeded, but these don't work. I've even tried to automatically scroll through the table, but perhaps I'm not doing in a way (using threads) that ensures that the scroll happens first, allowing the views to update, before the conversion takes place. I suspect this can't be done because I have found various questions asking this, and none have found a solution. Alternatively, can I just redraw my entire view in a UIImage, not requiring a UIView?
From Paul Hudson's website
Using any UIView that is not showing on the screen (say a row in a UITableview that is way down below the bottom of the screen.
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
You don't have to have a view in a window/on-screen to be able to render it into an image. I've done exactly this in PixelTest:
extension UIView {
/// Creates an image from the view's contents, using its layer.
///
/// - Returns: An image, or nil if an image couldn't be created.
func image() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.saveGState()
layer.render(in: context)
context.restoreGState()
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return image
}
}
This will render a view's layer into an image as it currently looks if it was to be rendered on-screen. That is to say, if the view hasn't been laid out yet, then it won't look the way you expect. PixelTest does this by force-laying out the view beforehand when verifying a view for snapshot testing.
You can also accomplish this using UIGraphicsImageRenderer.
extension UIView {
func image() -> UIImage {
let imageRenderer = UIGraphicsImageRenderer(bounds: bounds)
if let format = imageRenderer.format as? UIGraphicsImageRendererFormat {
format.opaque = true
}
let image = imageRenderer.image { context in
return layer.render(in: context.cgContext)
}
return image
}
}

Make emoji symbols grayscale in UILabel

I would like to use Apple's built-in emoji characters (specifically, several of the smileys, e.g. \ue415) in a UILabel but I would like the emojis to be rendered in grayscale.
I want them to remain characters in the UILabel (either plain text or attributed is fine). I'm not looking for a hybrid image / string solution (which I already have).
Does anyone know how to accomplish this?
I know you said you aren't looking for a "hybrid image solution", but I have been chasing this dragon for a while and the best result I could come up with IS a hybrid. Just in case my solution is somehow more helpful on your journey, I am including it here. Good luck!
import UIKit
import QuartzCore
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// the target label to apply the effect to
let label = UILabel(frame: view.frame)
// create label text with empji
label.text = "🍑 HELLO"
label.textAlignment = .center
// set to red to further show the greyscale change
label.textColor = .red
// calls our extension to get an image of the label
let image = UIImage.imageWithLabel(label: label)
// create a tonal filter
let tonalFilter = CIFilter(name: "CIPhotoEffectTonal")
// get a CIImage for the filter from the label image
let imageToBlur = CIImage(cgImage: image.cgImage!)
// set that image as the input for the filter
tonalFilter?.setValue(imageToBlur, forKey: kCIInputImageKey)
// get the resultant image from the filter
let outputImage: CIImage? = tonalFilter?.outputImage
// create an image view to show the result
let tonalImageView = UIImageView(frame: view.frame)
// set the image from the filter into the new view
tonalImageView.image = UIImage(ciImage: outputImage ?? CIImage())
// add the view to our hierarchy
view.addSubview(tonalImageView)
}
}
extension UIImage {
class func imageWithLabel(label: UILabel) -> UIImage {
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0.0)
label.layer.render(in: UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
}

Resources