├── .github └── workflows │ └── build.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Assets ├── cover-preview.png ├── textview-ui-ipad.gif └── textview-ui-iphone.gif ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ColumnTextViewUI │ ├── Core │ ├── ColumnTextView.swift │ └── WrapperTextView.swift │ └── Interoperatability SwiftUI │ ├── ColumnTextViewRepresentable.swift │ └── ColumnedTextView.swift └── Tests ├── ColumnTextViewUITests ├── ColumnTextViewUITests.swift └── XCTestManifests.swift └── LinuxMain.swift /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: macOS-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | 9 | - name: Switch to Xcode 11 10 | run: sudo xcode-select --switch /Applications/Xcode_11.3.app 11 | # Since we want to be running our tests from Xcode, we need to 12 | # generate an .xcodeproj file. Luckly, Swift Package Manager has 13 | # build in functionality to do so. 14 | - name: Generate xcodeproj 15 | run: swift package generate-xcodeproj 16 | - name: Run tests 17 | run: xcodebuild test -destination 'name=iPhone 11' -scheme 'ColumnTextViewUI-Package' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Assets/cover-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleev/column-text-view-ui/38f4179a5f177a9cbaeeabbf359dbd2f8b06b00c/Assets/cover-preview.png -------------------------------------------------------------------------------- /Assets/textview-ui-ipad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleev/column-text-view-ui/38f4179a5f177a9cbaeeabbf359dbd2f8b06b00c/Assets/textview-ui-ipad.gif -------------------------------------------------------------------------------- /Assets/textview-ui-iphone.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleev/column-text-view-ui/38f4179a5f177a9cbaeeabbf359dbd2f8b06b00c/Assets/textview-ui-iphone.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Astemir Eleev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ColumnTextViewUI", 8 | platforms: [ 9 | .iOS(.v12) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "ColumnTextViewUI", 15 | targets: ["ColumnTextViewUI"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "ColumnTextViewUI", 26 | dependencies: []), 27 | .testTarget( 28 | name: "ColumnTextViewUITests", 29 | dependencies: ["ColumnTextViewUI"]), 30 | ], 31 | swiftLanguageVersions: [ 32 | .v5 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # column-text-view-ui [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) 2 | 3 | [![Build](https://github.com/jvirus/column-text-view-ui/workflows/Build/badge.svg)]() 4 | [![Platform](https://img.shields.io/badge/Platform-iOS_12-yellow.svg)]() 5 | [![Platform](https://img.shields.io/badge/Platform-iPadOS_12-darkyellow.svg)]() 6 | [![Language](https://img.shields.io/badge/Language-Swift_5.1-orange.svg)]() 7 | [![SPM](https://img.shields.io/badge/SPM-Supported-red.svg)]() 8 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)]() 9 | 10 | **Last Update: 29/December/2019.** 11 | 12 | ![](/Assets/cover-preview.png) 13 | 14 | ### If you like the project, please give it a star ⭐ It will show the creator your appreciation and help others to discover the repo. 15 | 16 | # ✍️ About 17 | 📄 Column Text View is an adaptive UI component that renders text in columns, horizontally [iOS 12, UIKit, TextKit, SwiftUI]. 18 | 19 | # 📺 Demo 20 | Please wait while the `.gif` files are loading... 21 | 22 | 23 | 24 | 25 | # 🍱 Features 26 | - **Configurable** 27 | - There are a number of configurable properties and you can use, such as, you can specify the number of columns for the supplied text, specify column spacing, paddings and other properties 28 | - **Adaptive** 29 | - The underlying text container adjusts its size to support the needed amount of space for the supplied text 30 | - **SwiftUI Compatable** 31 | - You can use a dedicated wrapper called `ColumnedTextView` to use the componet with `SwiftUI` 32 | - **Ease of Use** 33 | - Instantiate a single instane of `ColumnTextView` or `ColumnedTextView` (for `SwiftUI` ), setup the parent view and supply some text 34 | - **Designable and Inspectable** 35 | - You can use `.storyboard` or `.xib` files to configure the component without touching code (well, almost) 36 | 37 | # 📚 Usage 38 | 39 | The first thing you need to do is to prepare the UI component. The following example demostrates the programmatic approach, where the component is instantiated without `.storyboard` or `.xib` outlets: 40 | 41 | ```swift 42 | // 1 43 | let columnTextView = ColumnTextView(frame: view.bounds, .columns(2)) 44 | columnTextView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 45 | columnTextView.backgroundColor = .white 46 | parentView.addSubview(columnTextView) 47 | 48 | // 2 49 | columnTextView.attributedText = attributedString 50 | ``` 51 | - (1) The first part is instantiation and setup. Here we have pretty usual things happenning, nothing exotic 🌴. You can specify the way the columns should be created. Here you have two options: to use `.absolute(***UInt16***)`, where the associated parameter is a single column `width` in points or to use `.columns(***UInt16***)`, where the associated parameter is a positive integer number that specifies the exact number of columns that should be created for the current screen's `width`. 52 | - (2) The next part is about the text `attachment`. You simply create an instance of `NSAttributedString`, configure it as you'd like and that's it. 53 | 54 | You can change the `attributedText` property, the results will be correspondigly reflected and specify all `four` paddings for `top, right, bottom and left` sides of the component. 55 | 56 | # 🏗 Installation 57 | 58 | ## Swift Package Manager 59 | 60 | ### Xcode 11+ 61 | 62 | 1. Open `MenuBar` → `File` → `Swift Packages` → `Add Package Dependency...` 63 | 2. Paste the package repository url `https://github.com/jVirus/column-text-view-ui` and hit `Next`. 64 | 3. Select the installment rules. 65 | 66 | After specifying which version do you want to install, the package will be downloaded and attached to your project. 67 | 68 | ### Package.swift 69 | If you already have a `Package.swift` or you are building your own package simply add a new dependency: 70 | 71 | ```swift 72 | dependencies: [ 73 | .package(url: "`https://github.com/jVirus/column-text-view-ui", from: "1.0.0") 74 | ] 75 | ``` 76 | 77 | ## Manual 78 | You can always use copy-paste the sources method 😄. Or you can compile the framework and include it with your project. 79 | 80 | # 🙋‍♀️🙋‍♂️ Contributing 81 | Your contributions are always appreciated. There are many ways how you help with the project: 82 | 83 | - You can suggest something 84 | - You can write additional documentation or sample codes 85 | - Implement a new feature 86 | - Fix a bug 87 | - Help to maintain by answering to the questions (if any) that other folks have 88 | - etc. 89 | 90 | Overall guidelies are: 91 | 92 | - Please, discuss a feature or a major source change/addition before spending time and creating a pool requested via issues. 93 | - Create a separate branch and make sure that your code compiles and does not produce errors and warnings. 94 | - Please, don't be upset if it takes a while to review your code or receive an answer. 95 | 96 | # 👨‍💻 Author 97 | [Astemir Eleev](https://github.com/jVirus) 98 | 99 | # 🔖 Licence 100 | The project is available under [MIT Licence](https://github.com/jVirus/column-text-view-ui/blob/master/LICENSE) 101 | -------------------------------------------------------------------------------- /Sources/ColumnTextViewUI/Core/ColumnTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColumnTextView.swift 3 | // ColumnTextViewUI 4 | // 5 | // Created by Astemir Eleev on 16.11.2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | final public class ColumnTextView: UIView { 13 | 14 | // MARK: - Private Properties 15 | 16 | private var columnWrapperTextView: WrapperTextView! 17 | private var scrollView: UIScrollView! 18 | 19 | // MARK: - Public Properties 20 | 21 | @IBInspectable public var attributedText: NSMutableAttributedString = .init() { 22 | didSet { 23 | columnWrapperTextView.attributedText = attributedText 24 | updateScrollViewContentSize() 25 | } 26 | } 27 | 28 | @IBInspectable public var font: UIFont? { 29 | didSet { 30 | columnWrapperTextView.font = font 31 | updateScrollViewContentSize() 32 | } 33 | } 34 | 35 | @IBInspectable public var fontSize: CGFloat = 0.0 { 36 | didSet { 37 | columnWrapperTextView.fontSize = fontSize 38 | updateScrollViewContentSize() 39 | } 40 | } 41 | 42 | @IBInspectable public var textAlignment: NSTextAlignment = .natural { 43 | didSet { 44 | columnWrapperTextView.textAlignment = textAlignment 45 | updateScrollViewContentSize() 46 | } 47 | } 48 | 49 | @IBInspectable public var textColor: UIColor? { 50 | didSet { 51 | columnWrapperTextView.textColor = textColor 52 | updateScrollViewContentSize() 53 | } 54 | } 55 | 56 | @IBInspectable public var interColumnMargin: CGFloat = 8 { 57 | didSet { 58 | columnWrapperTextView.interColumnMargin = interColumnMargin 59 | } 60 | } 61 | @IBInspectable public var leadingPadding: CGFloat = 16 { 62 | didSet { 63 | columnWrapperTextView.leadingPadding = leadingPadding 64 | } 65 | } 66 | @IBInspectable public var trailingPadding: CGFloat = 16 { 67 | didSet { 68 | columnWrapperTextView.trailingPadding = trailingPadding 69 | } 70 | } 71 | @IBInspectable public var topPadding: CGFloat = 32 { 72 | didSet { 73 | columnWrapperTextView.topPadding = topPadding 74 | } 75 | } 76 | @IBInspectable public var bottomPadding: CGFloat = 32 { 77 | didSet { 78 | columnWrapperTextView.bottomPadding = bottomPadding 79 | } 80 | } 81 | 82 | public var columns: WrapperTextView.ColumnUnit = .columns(2) { 83 | didSet { 84 | columnWrapperTextView.columns = columns 85 | } 86 | } 87 | 88 | public override var backgroundColor: UIColor? { 89 | didSet { 90 | columnWrapperTextView.backgroundColor = backgroundColor 91 | } 92 | } 93 | 94 | // MARK: - Initializers 95 | 96 | public init(frame: CGRect, _ columns: WrapperTextView.ColumnUnit) { 97 | self.columns = columns 98 | super.init(frame: frame) 99 | 100 | columnWrapperTextView = .init(frame: frame, columns: columns) 101 | 102 | scrollView = UIScrollView(frame: frame) 103 | scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 104 | 105 | scrollView.addSubview(columnWrapperTextView) 106 | addSubview(scrollView) 107 | 108 | updateScrollViewContentSize() 109 | } 110 | 111 | required init?(coder: NSCoder) { 112 | fatalError("init(coder:) has not been implemented") 113 | } 114 | 115 | // MARK: - Overrides 116 | 117 | public override func layoutSubviews() { 118 | super.layoutSubviews() 119 | 120 | columnWrapperTextView.frame = frame 121 | columnWrapperTextView.bounds = bounds 122 | columnWrapperTextView.layoutSubviews() 123 | updateScrollViewContentSize() 124 | } 125 | 126 | // MARK: - Private Utility 127 | 128 | private func updateScrollViewContentSize() { 129 | scrollView.contentSize = columnWrapperTextView.bounds.size 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/ColumnTextViewUI/Core/WrapperTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WrapperTextView.swift 3 | // ColumnTextViewUI 4 | // 5 | // Created by Astemir Eleev on 16.11.2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | final public class WrapperTextView: UIView { 13 | 14 | // MARK: - Private Properties 15 | 16 | private var storage: NSTextStorage! 17 | private var layout: NSLayoutManager! 18 | private var containers: [(storage: NSTextContainer, origin: CGPoint)] = [] 19 | 20 | // MARK: - Public Types 21 | 22 | public enum ColumnUnit { 23 | 24 | // MARK: - Cases 25 | 26 | /// Coluns per screen width 27 | case columns(UInt16) 28 | /// Absolute column width value 29 | case absolute(UInt16) 30 | 31 | // MARK: - Properties 32 | 33 | func getValue(interColumnMargin: CGFloat, bounds: CGRect, padding: (leading: CGFloat, trailing: CGFloat)) -> CGFloat { 34 | switch self { 35 | case .columns(let value): 36 | let margin = interColumnMargin * (CGFloat(value) - 1) 37 | return CGFloat((bounds.width - padding.leading - padding.trailing) - margin) / CGFloat(value) 38 | case .absolute(let value): 39 | return CGFloat(value) 40 | } 41 | } 42 | } 43 | 44 | // MARK: - Public Properties 45 | 46 | @IBInspectable public var attributedText: NSMutableAttributedString = .init(string: "") { 47 | didSet { 48 | commonInit() 49 | } 50 | } 51 | 52 | @IBInspectable public var interColumnMargin: CGFloat = 8 { 53 | didSet { 54 | setNeedsDisplay() 55 | } 56 | } 57 | @IBInspectable public var leadingPadding: CGFloat = 16 { 58 | didSet { 59 | setNeedsDisplay() 60 | } 61 | } 62 | @IBInspectable public var trailingPadding: CGFloat = 16 { 63 | didSet { 64 | setNeedsDisplay() 65 | } 66 | } 67 | @IBInspectable public var topPadding: CGFloat = 32 { 68 | didSet { 69 | setNeedsDisplay() 70 | } 71 | } 72 | @IBInspectable public var bottomPadding: CGFloat = 32 { 73 | didSet { 74 | setNeedsDisplay() 75 | } 76 | } 77 | 78 | public var columns: ColumnUnit = .columns(2) { 79 | didSet { 80 | setNeedsDisplay() 81 | createColumns() 82 | } 83 | } 84 | 85 | @IBInspectable public var font: UIFont? { 86 | didSet { 87 | guard let font = font else { return } 88 | 89 | updateStorage(with: .font) { (key, value) -> Any? in 90 | font 91 | } 92 | } 93 | } 94 | 95 | @IBInspectable public var fontSize: CGFloat = 0.0 { 96 | didSet { 97 | guard fontSize >= 0.0 else { return } 98 | 99 | updateStorage(with: .font) { (key, value) -> Any? in 100 | guard let font = value as? UIFont else { 101 | return nil 102 | } 103 | return font.withSize(fontSize) 104 | } 105 | } 106 | } 107 | 108 | @IBInspectable public var textAlignment: NSTextAlignment = .natural { 109 | didSet { 110 | updateStorage(with: .paragraphStyle) { (key, value) -> Any? in 111 | let paraStyle = NSMutableParagraphStyle() 112 | paraStyle.alignment = textAlignment 113 | return paraStyle 114 | } 115 | } 116 | } 117 | 118 | @IBInspectable public var textColor: UIColor? { 119 | didSet { 120 | updateStorage(with: .foregroundColor) { (key, value) -> Any? in 121 | textColor 122 | } 123 | } 124 | } 125 | 126 | private func updateStorage(with key: NSAttributedString.Key, closure: (_ key: NSAttributedString.Key, _ value: Any?) -> Any?) { 127 | let range: NSRange = .init(location: 0, length: attributedText.length) 128 | 129 | storage.enumerateAttribute(key, in: range, options: []) { (value, range, shouldStop) in 130 | if let attribute = closure(key, value) { 131 | storage.removeAttribute(key, range: range) 132 | storage.addAttribute(key, value: attribute, range: range) 133 | } 134 | } 135 | storage.edited(.editedAttributes, range: range, changeInLength: 0) 136 | storage.invalidateAttributes(in: range) 137 | 138 | setNeedsDisplay() 139 | createColumns() 140 | } 141 | 142 | // MARK: - Initializers 143 | 144 | public override init(frame: CGRect) { 145 | super.init(frame: frame) 146 | commonInit() 147 | } 148 | 149 | public init(frame: CGRect, columns: ColumnUnit) { 150 | super.init(frame: frame) 151 | self.columns = columns 152 | commonInit() 153 | } 154 | 155 | required init?(coder: NSCoder) { 156 | super.init(coder: coder) 157 | commonInit() 158 | } 159 | 160 | public override func awakeFromNib() { 161 | super.awakeFromNib() 162 | commonInit() 163 | } 164 | 165 | // MARK: - Overrides 166 | 167 | public override func draw(_ rect: CGRect) { 168 | for (container, point) in containers { 169 | let range = layout.glyphRange(for: container) 170 | layout.drawGlyphs(forGlyphRange: range, at: point) 171 | } 172 | super.draw(rect) 173 | } 174 | 175 | public override func layoutSubviews() { 176 | super.layoutSubviews() 177 | createColumns() 178 | setNeedsDisplay() 179 | } 180 | 181 | // MARK: - Private Methods 182 | 183 | private func commonInit() { 184 | layout = .init() 185 | storage = NSTextStorage(attributedString: attributedText) 186 | storage.addLayoutManager(layout) 187 | 188 | setNeedsDisplay() 189 | createColumns() 190 | } 191 | 192 | private func createColumns() { 193 | for _ in layout.textContainers { 194 | layout.removeTextContainer(at: 0) 195 | } 196 | containers.removeAll() 197 | 198 | let bounds = self.bounds 199 | let columnWidth = self.columns.getValue(interColumnMargin: interColumnMargin, 200 | bounds: superview?.bounds ?? bounds, 201 | padding: (leading: leadingPadding, trailing: trailingPadding)) 202 | let rowHeight = bounds.size.height - (bottomPadding + topPadding) 203 | 204 | let columnSize = CGSize(width: columnWidth, height: rowHeight) 205 | var x = bounds.origin.x + leadingPadding 206 | let y: CGFloat = topPadding 207 | 208 | var boundsWidth: CGFloat = 0 209 | 210 | while needsMoreColumns() { 211 | let container = NSTextContainer(size: columnSize) 212 | layout.addTextContainer(container) 213 | 214 | containers.append((container, .init(x: x, y: y))) 215 | 216 | let columnWithMargin = columnWidth + interColumnMargin 217 | x += columnWithMargin 218 | boundsWidth += columnWithMargin 219 | } 220 | boundsWidth += leadingPadding + trailingPadding 221 | 222 | self.bounds.size = .init(width: boundsWidth, height: bounds.height) 223 | frame.origin = .init(x: 0, y: frame.origin.y) 224 | } 225 | 226 | private func needsMoreColumns() -> Bool { 227 | guard let lastContainr = layout.textContainers.last else { 228 | // Always create at least one column 229 | return true 230 | } 231 | let range = layout.glyphRange(for: lastContainr) 232 | let glyphs = layout.numberOfGlyphs 233 | return range.location + range.length < glyphs 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Sources/ColumnTextViewUI/Interoperatability SwiftUI/ColumnTextViewRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColumnTextViewRepresentable.swift 3 | // ColumnTextViewUI 4 | // 5 | // Created by Astemir Eleev on 16.11.2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | #if canImport(SwiftUI) 10 | import SwiftUI 11 | #endif 12 | 13 | @available(iOS 13, *) 14 | public struct ColumnTextViewRepresentable: UIViewRepresentable { 15 | 16 | // MARK: - State Properties 17 | 18 | @State public private(set) var size: CGSize 19 | @State public private(set) var attributedString: NSMutableAttributedString 20 | @State public private(set) var backgroundColor: UIColor = .white 21 | @State public private(set) var font: UIFont? = nil 22 | @State public private(set) var fontSize: CGFloat = 0.0 23 | @State public private(set) var textColor: UIColor? = nil 24 | @State public private(set) var column: WrapperTextView.ColumnUnit 25 | 26 | // MARK: - Conformance to UIViewRepresentable protocol 27 | 28 | public typealias UIViewType = ColumnTextView 29 | 30 | public func makeUIView(context: UIViewRepresentableContext) -> UIViewType { 31 | .init(frame: .init(origin: .zero, size: size), column) 32 | } 33 | 34 | public func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext) { 35 | uiView.attributedText = attributedString 36 | uiView.backgroundColor = backgroundColor 37 | uiView.font = font 38 | uiView.fontSize = fontSize 39 | uiView.textColor = textColor 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ColumnTextViewUI/Interoperatability SwiftUI/ColumnedTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColumnedTextView.swift 3 | // ColumnTextViewUI 4 | // 5 | // Created by Astemir Eleev on 16.11.2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | #if canImport(SwiftUI) 10 | import SwiftUI 11 | #endif 12 | 13 | @available(iOS 13, *) 14 | public struct ColumnedTextView: View { 15 | 16 | // MARK: - State Porperties 17 | 18 | @State public private(set) var attributedString: NSMutableAttributedString = .init(string: "") 19 | @State public private(set) var font: UIFont? = nil 20 | @State public private(set) var fontSize: CGFloat = 0.0 21 | @State public private(set) var textColor: UIColor? = nil 22 | @State public private(set) var column: WrapperTextView.ColumnUnit 23 | 24 | // MARK: - Conformance to View protocol 25 | 26 | public var body: some View { 27 | GeometryReader { geometry in 28 | ColumnTextViewRepresentable( 29 | size: geometry.size, 30 | attributedString: self.attributedString, 31 | font: self.font, 32 | fontSize: self.fontSize, 33 | textColor: self.textColor, 34 | column: self.column 35 | ) 36 | } 37 | } 38 | } 39 | 40 | #if DEBUG 41 | 42 | @available(iOS 13, *) 43 | struct ColumnedTextView_Previews: PreviewProvider { 44 | 45 | static var previews: some View { 46 | ColumnedTextView(attributedString: text(), fontSize: 18, column: .columns(2)) 47 | .previewDevice(PreviewDevice(rawValue: "iPhone 11 Pro")) 48 | .previewDisplayName("iPhone 11 Pro") 49 | 50 | } 51 | } 52 | 53 | /// Test data 54 | fileprivate func text() -> NSMutableAttributedString { 55 | let paraStyle = NSMutableParagraphStyle() 56 | paraStyle.alignment = .justified 57 | 58 | let attributedString: NSMutableAttributedString = .init(string: """ 59 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Haec quo modo conveniant, non sane intellego. At, illa, ut vobis placet, partem quandam tuetur, reliquam deserit. Quo modo autem optimum, si bonum praeterea nullum est? Tu enim ista lenius, hic Stoicorum more nos vexat. Duo Reges: constructio interrete. Nam, ut sint illa vendibiliora, haec uberiora certe sunt. Teneo, inquit, finem illi videri nihil dolere. Quae cum dixisset paulumque institisset, Quid est? 60 | 61 | Ea possunt paria non esse. Non igitur de improbo, sed de callido improbo quaerimus, qualis Q. Dolor ergo, id est summum malum, metuetur semper, etiamsi non aderit; Reguli reiciendam; Quasi ego id curem, quid ille aiat aut neget. Urgent tamen et nihil remittunt. Qui ita affectus, beatum esse numquam probabis; Quis suae urbis conservatorem Codrum, quis Erechthei filias non maxime laudat? Quae diligentissime contra Aristonem dicuntur a Chryippo. Quicquid porro animo cernimus, id omne oritur a sensibus; 62 | 63 | Quoniam, si dis placet, ab Epicuro loqui discimus. Cuius quidem, quoniam Stoicus fuit, sententia condemnata mihi videtur esse inanitas ista verborum. Cur iustitia laudatur? Sed id ne cogitari quidem potest quale sit, ut non repugnet ipsum sibi. Cum id fugiunt, re eadem defendunt, quae Peripatetici, verba. 64 | 65 | Hoc non est positum in nostra actione. At iam decimum annum in spelunca iacet. Si enim, ut mihi quidem videtur, non explet bona naturae voluptas, iure praetermissa est; Tubulo putas dicere? 66 | 67 | Restinguet citius, si ardentem acceperit. Illum mallem levares, quo optimum atque humanissimum virum, Cn. Istam voluptatem perpetuam quis potest praestare sapienti? Recte dicis; Compensabatur, inquit, cum summis doloribus laetitia. Aut haec tibi, Torquate, sunt vituperanda aut patrocinium voluptatis repudiandum. Videamus animi partes, quarum est conspectus illustrior; 68 | 69 | Sint ista Graecorum; Sed ad bona praeterita redeamus. Quae fere omnia appellantur uno ingenii nomine, easque virtutes qui habent, ingeniosi vocantur. An eum locum libenter invisit, ubi Demosthenes et Aeschines inter se decertare soliti sunt? Hoc loco tenere se Triarius non potuit. Hinc ceteri particulas arripere conati suam quisque videro voluit afferre sententiam. Nonne videmus quanta perturbatio rerum omnium consequatur, quanta confusio? Minime vero istorum quidem, inquit. Vitiosum est enim in dividendo partem in genere numerare. Conferam avum tuum Drusum cum C. Sint modo partes vitae beatae. Qua tu etiam inprudens utebare non numquam. 70 | 71 | Non laboro, inquit, de nomine. Sin tantum modo ad indicia veteris memoriae cognoscenda, curiosorum. Sed tu istuc dixti bene Latine, parum plane. Aliud igitur esse censet gaudere, aliud non dolere. Ergo hoc quidem apparet, nos ad agendum esse natos. Si longus, levis. Quia dolori non voluptas contraria est, sed doloris privatio. Omnia contraria, quos etiam insanos esse vultis. 72 | 73 | Quo plebiscito decreta a senatu est consuli quaestio Cn. Facillimum id quidem est, inquam. Quantum Aristoxeni ingenium consumptum videmus in musicis? Duae sunt enim res quoque, ne tu verba solum putes. Itaque primos congressus copulationesque et consuetudinum instituendarum voluntates fieri propter voluptatem; Verum tamen cum de rebus grandioribus dicas, ipsae res verba rapiunt; 74 | 75 | Nobis Heracleotes ille Dionysius flagitiose descivisse videtur a Stoicis propter oculorum dolorem. Cupiditates non Epicuri divisione finiebat, sed sua satietate. Negat enim summo bono afferre incrementum diem. Scio enim esse quosdam, qui quavis lingua philosophari possint; Si id dicis, vicimus. Hoc tu nunc in illo probas. Quo tandem modo? Quicquid enim a sapientia proficiscitur, id continuo debet expletum esse omnibus suis partibus; Ita multo sanguine profuso in laetitia et in victoria est mortuus. Mihi, inquam, qui te id ipsum rogavi? 76 | 77 | Haec quo modo conveniant, non sane intellego. Heri, inquam, ludis commissis ex urbe profectus veni ad vesperum. Maximas vero virtutes iacere omnis necesse est voluptate dominante. Nihil enim hoc differt. 78 | 79 | Quid est, quod ab ea absolvi et perfici debeat? Quid est, quod ab ea absolvi et perfici debeat? Audio equidem philosophi vocem, Epicure, sed quid tibi dicendum sit oblitus es. Nec vero alia sunt quaerenda contra Carneadeam illam sententiam. Quamquam haec quidem praeposita recte et reiecta dicere licebit. Negat esse eam, inquit, propter se expetendam. Quae sequuntur igitur? Sequitur disserendi ratio cognitioque naturae; 80 | 81 | Eodem modo is enim tibi nemo dabit, quod, expetendum sit, id esse laudabile. Sic vester sapiens magno aliquo emolumento commotus cicuta, si opus erit, dimicabit. Huius ego nunc auctoritatem sequens idem faciam. Effluit igitur voluptas corporis et prima quaeque avolat saepiusque relinquit causam paenitendi quam recordandi. 82 | 83 | Eorum enim est haec querela, qui sibi cari sunt seseque diligunt. Stoicos roga. Sapiens autem semper beatus est et est aliquando in dolore; Quis istud, quaeso, nesciebat? O magnam vim ingenii causamque iustam, cur nova existeret disciplina! Perge porro. Si verbum sequimur, primum longius verbum praepositum quam bonum. Ne in odium veniam, si amicum destitero tueri. 84 | 85 | Ergo illi intellegunt quid Epicurus dicat, ego non intellego? Mihi quidem Antiochum, quem audis, satis belle videris attendere. Conferam tecum, quam cuique verso rem subicias; In his igitur partibus duabus nihil erat, quod Zeno commutare gestiret. 86 | 87 | An haec ab eo non dicuntur? Septem autem illi non suo, sed populorum suffragio omnium nominati sunt. Quae diligentissime contra Aristonem dicuntur a Chryippo. Eaedem enim utilitates poterunt eas labefactare atque pervertere. Quo studio Aristophanem putamus aetatem in litteris duxisse? Paulum, cum regem Persem captum adduceret, eodem flumine invectio? Serpere anguiculos, nare anaticulas, evolare merulas, cornibus uti videmus boves, nepas aculeis. Mihi enim erit isdem istis fortasse iam utendum. 88 | 89 | Quamquam haec quidem praeposita recte et reiecta dicere licebit. Quos quidem tibi studiose et diligenter tractandos magnopere censeo. An vero displicuit ea, quae tributa est animi virtutibus tanta praestantia? 90 | 91 | Compensabatur, inquit, cum summis doloribus laetitia. Vadem te ad mortem tyranno dabis pro amico, ut Pythagoreus ille Siculo fecit tyranno? Sed vos squalidius, illorum vides quam niteat oratio. Tum mihi Piso: Quid ergo? Egone non intellego, quid sit don Graece, Latine voluptas? Hanc quoque iucunditatem, si vis, transfer in animum; Itaque ab his ordiamur. Quae cum dixisset paulumque institisset, Quid est? Dat enim intervalla et relaxat. 92 | 93 | Neutrum vero, inquit ille. Bona autem corporis huic sunt, quod posterius posui, similiora. Plane idem, inquit, et maxima quidem, qua fieri nulla maior potest. Nam Pyrrho, Aristo, Erillus iam diu abiecti. Si stante, hoc natura videlicet vult, salvam esse se, quod concedimus; Quis est tam dissimile homini. 94 | 95 | Ad corpus diceres pertinere-, sed ea, quae dixi, ad corpusne refers? Quid igitur dubitamus in tota eius natura quaerere quid sit effectum? Ab his oratores, ab his imperatores ac rerum publicarum principes extiterunt. Tu vero, inquam, ducas licet, si sequetur; Nummus in Croesi divitiis obscuratur, pars est tamen divitiarum. Itaque hoc frequenter dici solet a vobis, non intellegere nos, quam dicat Epicurus voluptatem. 96 | 97 | Quorum sine causa fieri nihil putandum est. Sed nimis multa. Idem iste, inquam, de voluptate quid sentit? Unum est sine dolore esse, alterum cum voluptate. At Zeno eum non beatum modo, sed etiam divitem dicere ausus est. Cum praesertim illa perdiscere ludus esset. Nihil sane. 98 | """, attributes: [NSAttributedString.Key.paragraphStyle: paraStyle, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20)]) 99 | return attributedString 100 | } 101 | 102 | #endif 103 | -------------------------------------------------------------------------------- /Tests/ColumnTextViewUITests/ColumnTextViewUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ColumnTextViewUI 3 | 4 | final class ColumnTextViewUITests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | 11 | static var allTests = [ 12 | ("testExample", testExample), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Tests/ColumnTextViewUITests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ColumnTextViewUITests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ColumnTextViewUITests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ColumnTextViewUITests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------