SwiftUI NSAttributedString gradient foreground color - swiftui

I am using this code. How can I add GradientColor to strokeColor ?
strokeColor only accepts UIColor. Can I cast GradientColor to UIColor?
I want to do this.:
NSAttributedString.Key.strokeColor: LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing)
My Code:
struct StrokeTextLabel: UIViewRepresentable {
#Binding var fontName: String
#Binding var text: String
func makeUIView(context: Context) -> UILabel {
let attributedStringParagraphStyle = NSMutableParagraphStyle()
attributedStringParagraphStyle.alignment = NSTextAlignment.center
let attributedString = NSAttributedString(
string: text,
attributes:[
NSAttributedString.Key.paragraphStyle: attributedStringParagraphStyle,
NSAttributedString.Key.strokeWidth: 3.0,
NSAttributedString.Key.strokeColor: UIColor.red,// How can I change Gradient
NSAttributedString.Key.font: UIFont(name: fontName, size:30.0)!
]
)
let strokeLabel = UILabel(frame: CGRect.zero)
strokeLabel.attributedText = attributedString
strokeLabel.backgroundColor = UIColor.clear
strokeLabel.sizeToFit()
strokeLabel.center = CGPoint.init(x: 0.0, y: 0.0)
return strokeLabel
}
func updateUIView(_ uiView: UILabel, context: Context) { }
}

You can't use a gradient as a color; it isn't a color. A common technique in many situations is to use your text as a mask to reveal the gradient behind it. For example, that is an easy way to paint your text with a color-change that flows from top to bottom or from left to right.
A great deal, however, depends upon the exact details of what you want to do with the gradient.

Related

Render a SwiftUI view into an image with opacity

I need to render a SwiftUI view into an image with opacity, so that empty space of the view would be transparent when I layer the image above some background.
I use this code for conversion:
func convertViewToData<V>(view: V, size: CGSize) -> UIImage? where V: View {
guard let rootVC = UIApplication.shared.windows.first?.rootViewController else {
return nil
}
let imageVC = UIHostingController(rootView: view.edgesIgnoringSafeArea(.all))
imageVC.view.frame = CGRect(origin: .zero, size: size)
rootVC.view.insertSubview(imageVC.view, at: 0)
let uiImage = imageVC.view.asImage(size: size)
imageVC.view.removeFromSuperview()
return uiImage
}
extension UIView {
func asImage(size: CGSize) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.opaque = false
return UIGraphicsImageRenderer(bounds: bounds, format: format).image { context in
layer.render(in: context.cgContext)
}
}
}
extension View{
func convertToImage(size: CGSize) -> UIImage?{
convertViewToData(view: self, size: size)
}
}
And this code to test the resulting image:
struct ContentView: View {
var view: some View{
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
var body: some View {
HStack{
view
Image(uiImage: view.convertToImage(size: .init(width: 200, height: 200))!)
}
.background(LinearGradient(stops: [.init(color: .green, location: 0), .init(color: .red, location: 1)], startPoint: .bottom, endPoint: .top))
}
}
This code produces two instances of the text: the one on the left is layered on the gradient background, and the one on the right is on the black background:
Clearly, the transparent parts are replaced by the black color in the image.
I figured out that the alpha information is discarded somewhere in the convertViewToData, but I was not able to find a way to preserve it.

Is there an easy way to pinch to zoom and drag any View in SwiftUI?

I have been looking for a short, reusable piece of code that allows to zoom and drag any view in SwiftUI, and also to change the scale independently.
This would be the answer.
The interesting part that I add is that the scale of the zoomed View can be controled from outside via a binding property. So we don't need to depend just on the pinching gesture, but can add a double tap to get the maximum scale, return to the normal scale, or have a slider (for instance) that changes the scale as we please.
I owe the bulk of this code to jtbandes in his answer to this question.
Here you have in a single file the code of the Zoomable and Scrollable view and a Test View to show how it works:
`
import SwiftUI
let maxAllowedScale = 4.0
struct TestZoomableScrollView: View {
#State private var scale: CGFloat = 1.0
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded {
if scale < maxAllowedScale / 2 {
scale = maxAllowedScale
} else {
scale = 1.0
}
}
}
var body: some View {
VStack(alignment: .center) {
Spacer()
ZoomableScrollView(scale: $scale) {
Image("foto_producto")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
}
.frame(width: 300, height: 300)
.border(.black)
.gesture(doubleTapGesture)
Spacer()
Text("Change the scale")
Slider(value: $scale, in: 0.5...maxAllowedScale + 0.5)
.padding(.horizontal)
Spacer()
}
}
}
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
#Binding private var scale: CGFloat
init(scale: Binding<CGFloat>, #ViewBuilder content: () -> Content) {
self._scale = scale
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = maxAllowedScale
scrollView.minimumZoomScale = 1
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
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), scale: $scale)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
uiView.zoomScale = scale
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
#Binding var scale: CGFloat
init(hostingController: UIHostingController<Content>, scale: Binding<CGFloat>) {
self.hostingController = hostingController
self._scale = scale
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
self.scale = scale
}
}
}
`
I think it's the shortest, easiest way to get the desired behaviour. Also, it works perfectly, something that I haven't found in other solutions offered here. For example, the zooming out is smooth and usually it can be jerky if you don't use this approach.
The slider hast that range to show how the minimun and maximum values are respected, in a real app the range would be 1...maxAllowedScale.
As for the double tap, the behaviour can be changed very easily depending pm what you prefer.
I attach video to show everything at once:
I hope this helps anyone who's looking for this feature.

UIImageView Representable not resizing

I can't find a way to make a UIImageView wrapped in a UIViewRepresentable be sized to fit the frame. It always resizes beyond the screen no matter what content Mode or clipping or explcit framing I do. (The image dimensions are much larger than the device screen frame)
To clarify: I need to use UIImageView due to some subview positioning I have to do down the line and various other reasons.
Here's a paired down example:
struct ImageView: UIViewRepresentable {
var image: UIImage
func makeUIView(context: Context) -> some UIView {
let imageView = UIImageView()
imageView.image = image
imageView.backgroundColor = .red
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.frame = CGRect(x: 0, y: 0, width: 300, height: 400)
return imageView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
Then this is how I'm trying to implement it
struct ContentView: View {
var body: some View {
ImageView(image: UIImage(named: "full-ux-bg-image")!)
//.frame(width: 300, height: 400, alignment: .center) //This is just a test of explicit sizing
.padding()
}
}
Any ideas how to make this work? I want it to fit in the SwiftUI view without going over.
It has default constrains for content hugging/compression, to have possibility to manipulate with view externally we need to lowered those (... and never set frame for representable, just in case)
Here is fixed variant (tested with Xcode 14 / iOS 16)
func makeUIView(context: Context) -> some UIView {
let imageView = UIImageView()
imageView.image = image
imageView.backgroundColor = .red
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.setContentHuggingPriority(.defaultLow, for: .vertical)
imageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return imageView
}

Draw overlay on AVPlayer SwiftUI

I am building an app where I want to draw rectangles on a video. The rectangles will come from a neural network and the position will be relative to the top left corner och the image frame.
To achieve that, I need to figure out a way to draw overlays on the AVPlayer view and use the actual frame as reference.
Is there an easy way of doing it in SwiftUI (which I started to learn a couple of days ago)?
Code that I have now, this is just about drawing something the way I want it. I.e. I want to find out how to position the rectangle with respect to the image/video. The orange dot should be origin if the black rectangle is the video.
Current code:
import SwiftUI
import AVKit
struct PlayVideo: View {
var moviePath: URL?
var player: AVPlayer
init(fileName: String) {
moviePath = fileURL(for: fileName)
player = AVPlayer(url: moviePath!)
}
var body: some View {
VideoPlayer(player: player)
.scaledToFill()
.overlay(
Rectangle()
.fill(Color.blue)
.frame(width: 20, height: 20),
alignment: .center)
Button(action: {
player.rate = 0.04
}, label: {
Text("Play Slow")
})
}
}
I tried with .position(x: ,y: ), but that seems to be for the parent frame.
EDIT
I have tried a new approach:
import SwiftUI
import AVKit
var player = AVPlayer()
struct PlayVideo: View {
var moviePath: URL
init(fileName: String) {
moviePath = fileURL(for: fileName)
}
var body: some View {
PlayerView(url: moviePath)
Button(action: {
player.rate = 0.04
}, label: {
Text("Play Slow")
})
}
}
struct PlayVideo_Previews: PreviewProvider {
static var previews: some View {
PlayVideo(fileName: "path")
}
}
struct PlayerView: UIViewRepresentable {
var url_: URL
init(url: URL) {
url_ = url
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(frame: .zero, url: url_)
}
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
init(frame: CGRect, url: URL) {
super.init(frame: frame)
player = AVPlayer(url: url)
player.play()
playerLayer.player = player
layer.addSublayer(playerLayer)
// Create a CGRect object which is used to render a rectangle.
let rectFrame: CGRect = CGRect(x:CGFloat(0), y:CGFloat(0), width:CGFloat(100), height:CGFloat(50))
// Create a UIView object which use above CGRect object.
let greenView = UIView(frame: rectFrame)
// Set UIView background color.
greenView.backgroundColor = UIColor.green
layer.addSublayer(greenView.layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
As you can see, there is a problem with the offset of the video and the view.
I imagine one could do a second view and place on top of the video view with the same size and position. Then one can draw on that. But the problem remains how to get size and position of the video correctly.

Resizable UITextView Has sizeThatFits That Gives Wrong Size in UIViewRepresentable

I am using a UITextView in a SwiftUI app in order to get a list of editable, multiline text fields based on this answer: How do I create a multiline TextField in SwiftUI?
I use the component in SwiftUI like this:
#State private var textHeight: CGFloat = 0
...
GrowingField(text: $subtask.text ?? "", height: $textHeight, changed:{
print("Save...")
})
.frame(height: textHeight)
The GrowingField is defined like this:
struct GrowingField: UIViewRepresentable {
#Binding var text: String
#Binding var height: CGFloat
var changed:(() -> Void)?
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.isScrollEnabled = false
textView.backgroundColor = .orange //For debugging
//Set the font size and style...
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
if uiView.text != self.text{
uiView.text = self.text
}
recalculateHeight(textView: uiView, height: $height)
}
func recalculateHeight(textView: UITextView, height: Binding<CGFloat>) {
let newSize = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if height.wrappedValue != newSize.height {
DispatchQueue.main.async {
height.wrappedValue = newSize.height
}
}
}
//Coordinator and UITextView delegates...
}
The problem I'm having is that sizeThatFits calculates the correct height at first, then replaces it with an incorrect height. If I print the newSize inside recalculateHeight() it goes like this when my view loads:
(63.0, 34.333333333333336) <!-- Right
(3.0, 143.33333333333334) <!-- Wrong
(3.0, 143.33333333333334) <!-- Wrong
I have no idea where the wrong size is coming from, and I don't know why the right one is replaced. This is how it looks with the height being way too big:
If I make a change to it, the recalculateHeight() method gets called again via textViewDidChange() and it rights itself:
This is really hacky, but if I put a timer in makeUIView(), it fixes itself as well:
//Eww, gross...
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
recalculateHeight(view: textView, height: $height)
}
Any idea how I can determine where the incorrect sizeThatFits value is coming from and how I can fix it?
It took me a long time to arrive at a solution for this. It turns out the UITextView sizing logic is good. It was a parent animation that presents my views that was causing updateUIView to fire again with in-transition UITextView size values.
By setting .animation(.none) on the parent VStack that holds all my text fields, it stopped the propagation of the animation and now it works. 🙂