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..79b480f 100644 --- a/LoafExamples/LoafExamples/Examples.swift +++ b/LoafExamples/LoafExamples/Examples.swift @@ -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,28 @@ 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), + 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 c256f1a..f34d89e 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,15 @@ 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? + + /// Space between lines. Optional. If not provided, it will use the default operating system value. + let lineSpacing: CGFloat? public init( backgroundColor: UIColor, @@ -62,17 +74,25 @@ 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, + lineSpacing: CGFloat? = 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 + self.lineSpacing = lineSpacing } } @@ -225,6 +245,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 +264,37 @@ 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 + 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 + if usesIcon { + textWidth = textWidth - iconSize.width - spaceBetweenTextAndIcon + } + + let textHeight = max( + toast.message.heightWithConstrainedWidth(width: textWidth, font: font, lineSpacing: lineSpacing), + 40 + ) + preferredContentSize = CGSize(width: loafWidth, height: textHeight + contentInsets.top + contentInsets.bottom + 1) } required init?(coder aDecoder: NSCoder) { @@ -301,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)) @@ -320,7 +363,7 @@ final class LoafViewController: UIViewController { self?.loaf.completionHandler?(.tapped) } } - + private func constrainWithIconAlignment(_ alignment: Loaf.Style.IconAlignment, showsIcon: Bool = true) { view.addSubview(label) @@ -330,38 +373,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 { @@ -379,4 +443,3 @@ private struct Queue { } } } -