From 4e3c17bd73fd78d43789e6950e2d9dcbd85027d0 Mon Sep 17 00:00:00 2001 From: Ricardo Ruiz Lopez Date: Sun, 5 Mar 2023 16:13:46 +0000 Subject: [PATCH 1/3] Add more customisation options. --- .../LoafExamples/Base.lproj/Main.storyboard | 60 ++++++---- LoafExamples/LoafExamples/Examples.swift | 9 +- Sources/Loaf/Loaf.swift | 105 +++++++++++++----- 3 files changed, 125 insertions(+), 49 deletions(-) diff --git a/LoafExamples/LoafExamples/Base.lproj/Main.storyboard b/LoafExamples/LoafExamples/Base.lproj/Main.storyboard index 43c4e7c..e79a73b 100644 --- a/LoafExamples/LoafExamples/Base.lproj/Main.storyboard +++ b/LoafExamples/LoafExamples/Base.lproj/Main.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -16,13 +14,13 @@ - + - + @@ -39,7 +37,7 @@ - + @@ -56,7 +54,7 @@ - + @@ -73,7 +71,7 @@ - + @@ -94,7 +92,7 @@ - + @@ -111,7 +109,7 @@ - + @@ -132,7 +130,7 @@ - + @@ -149,7 +147,7 @@ - + @@ -166,7 +164,7 @@ - + @@ -183,7 +181,7 @@ - + @@ -204,7 +202,7 @@ - + @@ -221,7 +219,7 @@ - + @@ -238,7 +236,7 @@ - + @@ -254,6 +252,23 @@ + + + + + + + + + + + @@ -274,7 +289,7 @@ - + @@ -288,4 +303,9 @@ + + + + + diff --git a/LoafExamples/LoafExamples/Examples.swift b/LoafExamples/LoafExamples/Examples.swift index 518e7f3..4c819cb 100644 --- a/LoafExamples/LoafExamples/Examples.swift +++ b/LoafExamples/LoafExamples/Examples.swift @@ -12,7 +12,7 @@ import Loaf class Examples: UITableViewController { private enum Example: String, CaseIterable { - case success = "An action was successfully completed" + case success = "An action was successfully completed - An action was successfully completed" case error = "An error has occured" case warning = "A warning has occured" case info = "This is some information" @@ -28,11 +28,12 @@ class Examples: UITableViewController { case custom1 = "This will showcase using custom colors and font" case custom2 = "This will showcase using right icon alignment" case custom3 = "This will showcase using no icon and 80% screen size width" + case custom4 = "This will showcase custom spacing, content insets, and icon size" static let grouped: [[Example]] = [[.success, .error, .warning, .info], [.bottom, .top], [.vertical, .left, .right, .mix], - [.custom1, .custom2, .custom3]] + [.custom1, .custom2, .custom3, custom4]] } private var isDarkMode = false @@ -112,6 +113,10 @@ class Examples: UITableViewController { Loaf(example.rawValue, state: .custom(.init(backgroundColor: .purple, iconAlignment: .right)), sender: self).show() case .custom3: Loaf(example.rawValue, state: .custom(.init(backgroundColor: .black, icon: nil, textAlignment: .center, width: .screenPercentage(0.8))), sender: self).show() + case .custom4: + let redColor = UIColor(red: 204.0/255, green: 51.0/255, blue: 51.0/255, alpha: 1) + let lightRedColor = UIColor(red: 255.0/255, green: 238.0/255, blue: 238.0/255, alpha: 1) + Loaf(example.rawValue, state: .custom(.init(backgroundColor: lightRedColor, textColor: redColor, tintColor: redColor, icon: UIImage(named: "moon"), iconSize: CGSize(width: 16, height: 16), textAlignment: .left, iconAlignment: .right, width: .screenPercentage(0.8), spaceBetweenTextAndIcon: 16, contentInsets: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16))), sender: self).show() } tableView.deselectRow(at: indexPath, animated: true) diff --git a/Sources/Loaf/Loaf.swift b/Sources/Loaf/Loaf.swift index c256f1a..aad75ef 100644 --- a/Sources/Loaf/Loaf.swift +++ b/Sources/Loaf/Loaf.swift @@ -46,7 +46,10 @@ final public class Loaf { /// The icon on the loaf let icon: UIImage? - + + /// The size of the icon + let iconSize: CGSize? + /// The alignment of the text within the Loaf let textAlignment: NSTextAlignment @@ -55,6 +58,12 @@ final public class Loaf { /// The width of the loaf let width: Width + + /// When an icon is present, this is the separation between the icon and text. + let spaceBetweenTextAndIcon: CGFloat? + + /// Insets of the text plus the icon if provided. + let contentInsets: UIEdgeInsets? public init( backgroundColor: UIColor, @@ -62,17 +71,23 @@ final public class Loaf { tintColor: UIColor = .white, font: UIFont = .systemFont(ofSize: 14, weight: .medium), icon: UIImage? = Icon.info, + iconSize: CGSize? = nil, textAlignment: NSTextAlignment = .left, iconAlignment: IconAlignment = .left, - width: Width = .fixed(280)) { + width: Width = .fixed(280), + spaceBetweenTextAndIcon: CGFloat? = nil, + contentInsets: UIEdgeInsets? = nil) { self.backgroundColor = backgroundColor self.textColor = textColor self.tintColor = tintColor self.font = font self.icon = icon + self.iconSize = iconSize self.textAlignment = textAlignment self.iconAlignment = iconAlignment self.width = width + self.spaceBetweenTextAndIcon = spaceBetweenTextAndIcon + self.contentInsets = contentInsets } } @@ -225,6 +240,11 @@ protocol LoafDelegate: AnyObject { } final class LoafViewController: UIViewController { + + static let defaultIconSize = CGSize(width: 28, height: 28) + static let defaultSpaceBetweenTextAndIcon: CGFloat = 10.0 + static let defaultContentInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) + var loaf: Loaf let label = UILabel() @@ -239,22 +259,32 @@ final class LoafViewController: UIViewController { self.transDelegate = Manager(loaf: toast, size: .zero) super.init(nibName: nil, bundle: nil) - var width: CGFloat? + var loafWidth: CGFloat = 280 if case let Loaf.State.custom(style) = loaf.state { self.font = style.font self.textAlignment = style.textAlignment switch style.width { case .fixed(let value): - width = value + loafWidth = value case .screenPercentage(let percentage): guard 0...1 ~= percentage else { return } - width = UIScreen.main.bounds.width * percentage + loafWidth = UIScreen.main.bounds.width * percentage } } - - let height = max(toast.message.heightWithConstrainedWidth(width: 240, font: font) + 12, 40) - preferredContentSize = CGSize(width: width ?? 280, height: height) + + var usesIcon = true + if case let Loaf.State.custom(style) = loaf.state { + usesIcon = style.icon != nil + } + + var textWidth: CGFloat = loafWidth - contentInsets.left - contentInsets.right + if usesIcon { + textWidth = textWidth - iconSize.width - spaceBetweenTextAndIcon + } + + let textHeight = max(toast.message.heightWithConstrainedWidth(width: textWidth, font: font), 40) + preferredContentSize = CGSize(width: loafWidth, height: textHeight + contentInsets.top + contentInsets.bottom + 1) } required init?(coder aDecoder: NSCoder) { @@ -320,7 +350,7 @@ final class LoafViewController: UIViewController { self?.loaf.completionHandler?(.tapped) } } - + private func constrainWithIconAlignment(_ alignment: Loaf.Style.IconAlignment, showsIcon: Bool = true) { view.addSubview(label) @@ -330,38 +360,59 @@ final class LoafViewController: UIViewController { switch alignment { case .left: NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: contentInsets.left), imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - imageView.heightAnchor.constraint(equalToConstant: 28), - imageView.widthAnchor.constraint(equalToConstant: 28), + imageView.heightAnchor.constraint(equalToConstant: iconSize.height), + imageView.widthAnchor.constraint(equalToConstant: iconSize.width), - label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10), - label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -4), - label.topAnchor.constraint(equalTo: view.topAnchor), - label.bottomAnchor.constraint(equalTo: view.bottomAnchor) + label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: spaceBetweenTextAndIcon), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -contentInsets.right), + label.topAnchor.constraint(equalTo: view.topAnchor, constant: contentInsets.top), + label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -contentInsets.bottom) ]) case .right: NSLayoutConstraint.activate([ - imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), + imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -contentInsets.right), imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - imageView.heightAnchor.constraint(equalToConstant: 28), - imageView.widthAnchor.constraint(equalToConstant: 28), + imageView.heightAnchor.constraint(equalToConstant: iconSize.height), + imageView.widthAnchor.constraint(equalToConstant: iconSize.width), - label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), - label.trailingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: -4), - label.topAnchor.constraint(equalTo: view.topAnchor), - label.bottomAnchor.constraint(equalTo: view.bottomAnchor) + label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: contentInsets.left), + label.trailingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: -spaceBetweenTextAndIcon), + label.topAnchor.constraint(equalTo: view.topAnchor, constant: contentInsets.top), + label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -contentInsets.bottom) ]) } } else { NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), - label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), - label.topAnchor.constraint(equalTo: view.topAnchor), - label.bottomAnchor.constraint(equalTo: view.bottomAnchor) + label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: contentInsets.left), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -contentInsets.right), + label.topAnchor.constraint(equalTo: view.topAnchor, constant: contentInsets.top), + label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -contentInsets.bottom) ]) } } + + private var iconSize: CGSize { + if case let .custom(style) = loaf.state { + return style.iconSize ?? Self.defaultIconSize + } + return Self.defaultIconSize + } + + private var spaceBetweenTextAndIcon: CGFloat { + if case let .custom(style) = loaf.state { + return style.spaceBetweenTextAndIcon ?? Self.defaultSpaceBetweenTextAndIcon + } + return Self.defaultSpaceBetweenTextAndIcon + } + + private var contentInsets: UIEdgeInsets { + if case let .custom(style) = loaf.state { + return style.contentInsets ?? Self.defaultContentInsets + } + return Self.defaultContentInsets + } } private struct Queue { From 964d274e096d93fb1a5653bb17d7258f1057860b Mon Sep 17 00:00:00 2001 From: Ricardo Ruiz Lopez Date: Sun, 5 Mar 2023 16:17:55 +0000 Subject: [PATCH 2/3] Restore string. --- LoafExamples/LoafExamples/Examples.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LoafExamples/LoafExamples/Examples.swift b/LoafExamples/LoafExamples/Examples.swift index 4c819cb..e1f75bc 100644 --- a/LoafExamples/LoafExamples/Examples.swift +++ b/LoafExamples/LoafExamples/Examples.swift @@ -12,7 +12,7 @@ import Loaf class Examples: UITableViewController { private enum Example: String, CaseIterable { - case success = "An action was successfully completed - An action was successfully completed" + case success = "An action was successfully completed" case error = "An error has occured" case warning = "A warning has occured" case info = "This is some information" From 454c19c0c713245c9052f2a50a9f044dff08328e Mon Sep 17 00:00:00 2001 From: Ricardo Ruiz Lopez Date: Thu, 25 May 2023 09:57:34 +0100 Subject: [PATCH 3/3] Add line spacing option. --- LoafExamples/LoafExamples/Examples.swift | 20 ++++++++++- Sources/Loaf/Extensions.swift | 44 ++++++++++++++++++++++-- Sources/Loaf/Loaf.swift | 18 ++++++++-- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/LoafExamples/LoafExamples/Examples.swift b/LoafExamples/LoafExamples/Examples.swift index e1f75bc..79b480f 100644 --- a/LoafExamples/LoafExamples/Examples.swift +++ b/LoafExamples/LoafExamples/Examples.swift @@ -116,7 +116,25 @@ class Examples: UITableViewController { case .custom4: let redColor = UIColor(red: 204.0/255, green: 51.0/255, blue: 51.0/255, alpha: 1) let lightRedColor = UIColor(red: 255.0/255, green: 238.0/255, blue: 238.0/255, alpha: 1) - Loaf(example.rawValue, state: .custom(.init(backgroundColor: lightRedColor, textColor: redColor, tintColor: redColor, icon: UIImage(named: "moon"), iconSize: CGSize(width: 16, height: 16), textAlignment: .left, iconAlignment: .right, width: .screenPercentage(0.8), spaceBetweenTextAndIcon: 16, contentInsets: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16))), sender: self).show() + Loaf( + example.rawValue, + state: .custom( + .init( + backgroundColor: lightRedColor, + textColor: redColor, + tintColor: redColor, + icon: UIImage(named: "moon"), + iconSize: CGSize(width: 16, height: 16), + textAlignment: .left, + iconAlignment: .right, + width: .screenPercentage(0.8), + spaceBetweenTextAndIcon: 16, + contentInsets: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16), + lineSpacing: 10.0 + ) + ), + sender: self + ).show() } tableView.deselectRow(at: indexPath, animated: true) diff --git a/Sources/Loaf/Extensions.swift b/Sources/Loaf/Extensions.swift index 8dba5ff..9b714cb 100644 --- a/Sources/Loaf/Extensions.swift +++ b/Sources/Loaf/Extensions.swift @@ -20,9 +20,22 @@ extension UIViewController { } extension String { - func heightWithConstrainedWidth(width: CGFloat, font: UIFont) -> CGFloat { + func heightWithConstrainedWidth(width: CGFloat, font: UIFont, lineSpacing: CGFloat?) -> CGFloat { + var attributes: [NSAttributedString.Key : Any] = [:] + if let lineSpacing = lineSpacing { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = lineSpacing + attributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle + } + attributes[NSAttributedString.Key.font] = font + let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) - let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil) + let boundingBox = self.boundingRect( + with: constraintRect, + options: [.usesLineFragmentOrigin, .usesFontLeading], + attributes: attributes, + context: nil + ) return boundingBox.height } } @@ -46,3 +59,30 @@ extension UIColor { self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) } } + +extension UILabel { + func setLineSpacing(lineSpacing: CGFloat) { + // Create a mutable attributed string with the label's text + guard let labelText = text else { return } + let attributedString = NSMutableAttributedString(string: labelText) + + // Create an instance of NSMutableParagraphStyle to configure the paragraph style + let paragraphStyle = NSMutableParagraphStyle() + + // Set the desired line spacing value + paragraphStyle.lineSpacing = lineSpacing + + // Preserve existing label properties + let labelFont = font ?? UIFont.systemFont(ofSize: 17.0) // Default font size + let labelColor = textColor ?? UIColor.black + let labelAlignment = textAlignment + + // Apply the label properties to the attributed string + attributedString.addAttribute(.font, value: labelFont, range: NSMakeRange(0, attributedString.length)) + attributedString.addAttribute(.foregroundColor, value: labelColor, range: NSMakeRange(0, attributedString.length)) + attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedString.length)) + + // Set the attributed string and preserved properties on the label + self.attributedText = attributedString + } +} diff --git a/Sources/Loaf/Loaf.swift b/Sources/Loaf/Loaf.swift index aad75ef..f34d89e 100644 --- a/Sources/Loaf/Loaf.swift +++ b/Sources/Loaf/Loaf.swift @@ -64,6 +64,9 @@ final public class Loaf { /// Insets of the text plus the icon if provided. let contentInsets: UIEdgeInsets? + + /// Space between lines. Optional. If not provided, it will use the default operating system value. + let lineSpacing: CGFloat? public init( backgroundColor: UIColor, @@ -76,7 +79,8 @@ final public class Loaf { iconAlignment: IconAlignment = .left, width: Width = .fixed(280), spaceBetweenTextAndIcon: CGFloat? = nil, - contentInsets: UIEdgeInsets? = nil) { + contentInsets: UIEdgeInsets? = nil, + lineSpacing: CGFloat? = nil) { self.backgroundColor = backgroundColor self.textColor = textColor self.tintColor = tintColor @@ -88,6 +92,7 @@ final public class Loaf { self.width = width self.spaceBetweenTextAndIcon = spaceBetweenTextAndIcon self.contentInsets = contentInsets + self.lineSpacing = lineSpacing } } @@ -274,8 +279,10 @@ final class LoafViewController: UIViewController { } var usesIcon = true + var lineSpacing: CGFloat? if case let Loaf.State.custom(style) = loaf.state { usesIcon = style.icon != nil + lineSpacing = style.lineSpacing } var textWidth: CGFloat = loafWidth - contentInsets.left - contentInsets.right @@ -283,7 +290,10 @@ final class LoafViewController: UIViewController { textWidth = textWidth - iconSize.width - spaceBetweenTextAndIcon } - let textHeight = max(toast.message.heightWithConstrainedWidth(width: textWidth, font: font), 40) + let textHeight = max( + toast.message.heightWithConstrainedWidth(width: textWidth, font: font, lineSpacing: lineSpacing), + 40 + ) preferredContentSize = CGSize(width: loafWidth, height: textHeight + contentInsets.top + contentInsets.bottom + 1) } @@ -331,6 +341,9 @@ final class LoafViewController: UIViewController { label.textColor = style.textColor label.font = style.font constrainWithIconAlignment(style.iconAlignment, showsIcon: imageView.image != nil) + if let lineSpacing = style.lineSpacing { + label.setLineSpacing(lineSpacing: lineSpacing) + } } let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) @@ -430,4 +443,3 @@ private struct Queue { } } } -