This has been asked before on Stackoverflow but the solutions don't work for me.
I'm building an app in SwiftUI but I need to use MapKit for maps, so I'm creating some views in UIKit (programmatically, no interface builder). My current problem is that I want to make a capsule background with some padding around a UILabel. It needs to accommodate arbitrary text, so I can't hardcode the sizes, but I can't find a way to determine the innate size of the UILabel text at runtime. I've tried UIEdgeInsets without success.
In the attached code, I am showing a SwiftUI version of what I'm trying to achieve, and then the UIKit attempt. I'd like to follow best practices, so please feel free to tell me any better ways of achieving this.
(Screenshot of what the code produces)
import SwiftUI
struct SwiftUICapsuleView: View {
var body: some View {
Text("Hello, World!")
.padding(6)
.background(Color.gray)
.cornerRadius(15)
}
}
struct UIKitCapsuleView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
let label = UILabel(frame: .zero)
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 17)
label.text = "Goodbye, World!"
label.layer.cornerRadius = 15
label.layer.backgroundColor = UIColor.gray.cgColor
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
label.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
label.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
return view
}
func updateUIView(_ view: UIView, context: Context) {
}
}
struct ExperimentView_Previews: PreviewProvider {
static var previews: some View {
VStack {
SwiftUICapsuleView()
UIKitCapsuleView()
}
}
}
You probably want to set the background color and rounded corners of the view, not the label.
You also should use a full set of constraints.
Give this a try:
struct UIKitCapsuleView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
// set the view's background color
view.backgroundColor = .cyan
// set the cornerRadius on the view's layer
view.layer.cornerRadius = 15
let label = UILabel(frame: .zero)
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 17)
label.text = "Goodbye, World!"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
// you can adjust padding here
let padding: CGFloat = 6
// use full constraints
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: view.topAnchor, constant: padding),
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding),
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -padding),
])
return view
}
func updateUIView(_ view: UIView, context: Context) {
}
}
Related
I am making a swiftUI app. I have some scrollable content for which I am using UIScrollView (Not using SwiftUI ScrollView due to some limitations). I have embedded my UIScrollView in a VStack. Above UIScrollView inside VStack I have a swiftUI view which has a tap gesture. My UIScrollView subview also has a tap gesture and UIScrollView subview can scroll over SwiftUI view inside VStack.
Problem:
When I scroll up and UIScrollView covers the SwiftUI View I can not tap on the portion of subview which is outside of the clipping bounds of UIScrollView, My SwiftUI View gets tapped instead, even when its not visible.
I have attached my sample code and some Images.
Code:
UIScrollViewWrapper
struct UIScrollViewWrapper<Content: View>: UIViewRepresentable {
var width: CGFloat
var height: CGFloat
#ViewBuilder var content: () -> Content
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.clipsToBounds = false
scrollView.delegate = context.coordinator
scrollView.isScrollEnabled = true
scrollView.showsVerticalScrollIndicator = false
let child = UIHostingController(rootView: content())
child.view.backgroundColor = .clear
context.coordinator.hostingController = child
scrollView.addSubview(child.view)
let newSize = child.view.sizeThatFits(CGSize(width: width, height: height))
child.view.frame = CGRect(origin: .zero, size: newSize)
scrollView.contentSize = newSize
return scrollView
}
func updateUIView(_ scrollView: UIScrollView, context: Context) {
context.coordinator.hostingController?.rootView = content()
if let child = context.coordinator.hostingController {
let newSize = child.view.sizeThatFits(CGSize(width: width, height: height))
child.view.frame = CGRect(origin: .zero, size: newSize)
scrollView.contentSize = newSize
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var parent: UIScrollViewWrapper
var hostingController: UIHostingController<Content>!
init(_ parent: UIScrollViewWrapper) {
self.parent = parent
}
}
}
ContentView
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
RoundedRectangle(cornerRadius: 15)
.fill(.cyan)
.overlay(
Text("SwiftUI View")
.foregroundColor(.white)
)
.frame(height: 75)
.onTapGesture {
print("SwiftUI")
}
UIScrollViewWrapper(width: geometry.size.width, height: geometry.size.height - 75) {
RoundedRectangle(cornerRadius: 15)
.fill(.mint)
.frame(height: geometry.size.height * 2)
.onTapGesture {
print("UIScrollView")
}
}
.border(.red, width: 2)
}
}
}
}
Things I have tried:
I tried making UIScrollView fullscreen and offsetting the subview so my SwiftUI view is visible. If I do this touches to my SwiftUI view gets blocked by UIScrollView. With this approach I have tried changing the zIndex of SwiftUI View putting it on top of UIScrollView when necessary but this is not working with my animations and giving bad user experience.
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .top) {
RoundedRectangle(cornerRadius: 15)
.fill(.cyan)
.overlay(
Text("SwiftUI View")
.foregroundColor(.white)
)
.frame(height: 75)
.onTapGesture {
print("SwiftUI")
}
UIScrollViewWrapper(width: geometry.size.width, height: geometry.size.height) {
RoundedRectangle(cornerRadius: 15)
.fill(.mint)
.frame(height: geometry.size.height * 2)
.offset(y: 75)
.onTapGesture {
print("UIScrollView")
}
}
.border(.red, width: 2)
}
}
}
}
Can you help me solve this problem ?
Thank You !
To do this with UIKit...
constrain the scroll view to fill the screen (safe area)
constrain the "sticky top view" to the scroll view's Frame Layout Guide
constrain the "scrollable content view" to the scroll view's Content Layout Guide
This will keep the top view "stuck in place" while allowing the content view to scroll normally.
Quick example:
class StickyTopScrollView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scrollView = UIScrollView()
let stickyTopView = UIView()
let contentView = UIView()
// label to add to the sticky top view
let stvLabel = UILabel()
stvLabel.font = .systemFont(ofSize: 26, weight: .light)
stvLabel.textAlignment = .center
stvLabel.textColor = .white
stvLabel.text = "Sticky Top View"
// vertical stack view to add to the "scrollable content" view
// to which we'll add some labels
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 32
scrollView.backgroundColor = .systemRed
stickyTopView.backgroundColor = .systemGreen
contentView.backgroundColor = .systemBlue
[scrollView, stickyTopView, contentView, stvLabel, stackView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
stickyTopView.addSubview(stvLabel)
contentView.addSubview(stackView)
scrollView.addSubview(stickyTopView)
scrollView.addSubview(contentView)
view.addSubview(scrollView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
let topViewHeight: CGFloat = 75.0
NSLayoutConstraint.activate([
// scroll view fills the safe area
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// center label in stick top view
stvLabel.centerXAnchor.constraint(equalTo: stickyTopView.centerXAnchor),
stvLabel.centerYAnchor.constraint(equalTo: stickyTopView.centerYAnchor),
// sticky top view should be full width and "stuck" to the top of the
// scroll view Frame Layout Guide
stickyTopView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0),
stickyTopView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
stickyTopView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
stickyTopView.heightAnchor.constraint(equalToConstant: topViewHeight),
// constrain "scrollable content" view to Content Layout Guide
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: topViewHeight),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// constrain "scrollable content" view Width to Frame Layout Guide
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
// constrain stack view in "scrollable content" view with
// 20-points "padding" on all 4 sides
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
])
// now, let's add some labels to the stack view so our
// "scrollable content" view will be tall enough to need to scroll
for i in 1...20 {
let v = UILabel()
v.font = .systemFont(ofSize: 26, weight: .light)
v.text = "Label \(i)"
v.backgroundColor = .yellow
stackView.addArrangedSubview(v)
}
// a little styling
stickyTopView.layer.cornerRadius = 16
contentView.layer.cornerRadius = 16
// and add tap gesture recognizers
stickyTopView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(stickyTopTapped(_:))))
contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(scrollContentTapped(_:))))
}
#objc func stickyTopTapped(_ g: UITapGestureRecognizer) {
print("Sticky Top View Tapped")
}
#objc func scrollContentTapped(_ g: UITapGestureRecognizer) {
print("Scrollable Content View Tapped")
}
}
Looks like this when running and scrolling:
While the "sticky top view" is visible, it can be tapped. As we "cover" it with the scrollable content view, the content view will get the tap.
You may be able to adapt that to SwiftUI with a UIScrollView in a UIViewRepresentable object.
When I place a label with centered text in a stack view, then update the text, it no longer appears centered.
import PlaygroundSupport
import UIKit
let stackView = UIStackView(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 20)))
let label = UILabel(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 20)))
label.text = "Hello"
label.textAlignment = .center
stackView.addArrangedSubview(label)
label.text = "World"
PlaygroundPage.current.liveView = stackView
The expected outcome is that "World" is centered. However, it appears leftaligned. Calling layoutSubviews() did not help.
Stack views don't play well without auto-layout - particularly in Playgrounds.
Try it like this:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let stackView = UIStackView()
let label = UILabel()
label.text = "Hello"
label.textAlignment = .center
// so we can see the frame
label.backgroundColor = .green
stackView.addArrangedSubview(label)
label.text = "World"
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.widthAnchor.constraint(equalToConstant: 100.0),
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20.0),
])
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
I have a UIButton and a UILabel displayed inline. They have different size fonts, however I would like to align them so they appear on the same line.
At the moment the UILabel is slight above the baseline of the UIButton.
I was hoping to avoid manually setting a content offset as I want this to scale correctly where possible. I worry manual calculations may have unexpected side effects on changing font sizes etc.
I have created a playground that should show the 2 elements:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
lazy var nameButton = configure(UIButton(type: .system), using: {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.titleLabel?.font = .systemFont(ofSize: 18)
$0.setTitleColor(.darkGray, for: .normal)
$0.contentHorizontalAlignment = .leading
$0.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
$0.backgroundColor = .lightGray
$0.setTitle("This is a button", for: .normal)
})
lazy var publishedDateLabel = configure(UILabel(frame: .zero), using: {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.font = .systemFont(ofSize: 14)
$0.textColor = .darkGray
$0.setContentHuggingPriority(UILayoutPriority.defaultLow, for: .horizontal)
$0.backgroundColor = .lightGray
$0.text = "and this is a label"
})
override func loadView() {
let view = UIView()
view.backgroundColor = .white
[nameButton, publishedDateLabel].forEach(view.addSubview(_:))
NSLayoutConstraint.activate([
nameButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
nameButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
publishedDateLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
publishedDateLabel.leadingAnchor.constraint(equalTo: nameButton.trailingAnchor),
publishedDateLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8)
])
self.view = view
}
// setup helper method
func configure<T>(_ value: T, using closure: (inout T) throws -> Void) rethrows -> T {
var value = value
try closure(&value)
return value
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
I have tried making the label and button the same height by adding publishedDateLabel.heightAnchor.constraint(equalTo: nameButton.heightAnchor)
This didn't change the alignment however.
I also tried using publishedDateLabel.lastBaselineAnchor.constraint(equalTo: nameButton.lastBaselineAnchor)
to align the anchors however this aligned the top of the elements
How can align the bottom of the text in the button to the bottom of the text in the label?
Just comment out the heightAnchor use the lastBaselineAnchor:
NSLayoutConstraint.activate([
nameButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
nameButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
publishedDateLabel.lastBaselineAnchor.constraint(equalTo: nameButton.lastBaselineAnchor),
publishedDateLabel.leadingAnchor.constraint(equalTo: nameButton.trailingAnchor),
publishedDateLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8)
])
In SwiftUI, you could easily achieving padding via the following code:
Text("Hello World!")
.padding(20)
What options do I have to achieve the same in UIKit?
You can use storyboard for same and easily make this layout by combining UIView and Label.
Just add once view in view controller and centre align vertically and horizontally. Then add label inside that view and set top, bottom, leading, trailing to 20 respective to that view.
Just check following image.
This will make exactly same output as you want.
Using a UIStackView, something like this:
class PaddingableView: UIView {
var padding = UIEdgeInsets.zero { didSet { contentStackView.layoutMargins = padding } }
// MARK: Subviews
private lazy var contentStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fill
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = padding
return stackView
}()
// MARK: Functions
private func addContentStackView() {
super.addSubview(contentStackView)
NSLayoutConstraint.activate([
contentStackView.topAnchor.constraint(equalTo: topAnchor),
contentStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}
override func addSubview(_ view: UIView) {
contentStackView.addArrangedSubview(view)
}
override func layoutSubviews() {
if contentStackView.superview == nil { addContentStackView() }
super.layoutSubviews()
}
}
Note that using too many nested stackviews can hit the performance
Ok, most simple is of corse Storyboard-based approach (as shown by #Sagar_Chauhan)...
Below is provided line-by-line Playground variant (as to compare with SwiftUI Preview by lines of code, for instance).
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 677))
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
self.view = view
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Hello World!"
label.textColor = .black
label.backgroundColor = .yellow
label.textAlignment = .center
self.view.addSubview(label)
let fitSize = label.sizeThatFits(view.bounds.size)
label.widthAnchor.constraint(equalToConstant: fitSize.width + 12.0).isActive = true
label.heightAnchor.constraint(equalToConstant: fitSize.height + 12.0).isActive = true
label.topAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
}
}
PlaygroundPage.current.liveView = MyViewController()
I have searched the internet thoroughly and cannot find how to adjust how high up the navigationItem.title appears. I am referring to the title that shows up in the middle of the navigation bar.
Use setTitleVerticalPositionAdjustment function of UINavigationBar which will let you set an required vertical offset (can be positive or negative) for navigation bar title. Read more about it here.
Good luck :)
Tomas' answer is the simplest one. Except of that you could use a custom titleView. That way you can inset the text horizontally as well as vertically:
class TitleView: UIView {
private let label: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 15)
return label
}()
var title: String? {
get { return label.text }
set { label.text = newValue }
}
convenience init(title: String, inset: UIEdgeInsets) {
self.init()
label.text = title
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: topAnchor, constant: inset.top).isActive = true
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset.left).isActive = true
bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: inset.bottom).isActive = true
trailingAnchor.constraint(equalTo: label.trailingAnchor, constant: inset.right).isActive = true
}
}
Then you can simply use it like this:
let titleView = TitleView(title: "TitleView", inset: .init(top: -10, left: 0, bottom: 0, right: 0))
vc.navigationItem.titleView = titleView