├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ExpandableText │ ├── ExpandableText.swift │ └── Extensions.swift └── Tests └── ExpandableTextTests └── ExpandableTextTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 NuPlay 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.5 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: "ExpandableText", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "ExpandableText", 15 | targets: ["ExpandableText"]), 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 this package depends on. 24 | .target( 25 | name: "ExpandableText", 26 | dependencies: []), 27 | .testTarget( 28 | name: "ExpandableTextTests", 29 | dependencies: ["ExpandableText"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

ExpandableText 😎 (SwiftUI)

2 |

3 | 4 | version 5 | 6 | 7 | Swift: 5.1+ 8 | 9 | 10 | iOS: 13.0+ 11 | 12 | 13 | license 14 |

15 |

Expand the text with the "more" button

16 |

example

17 | 18 | 19 | 20 | ## Get Started 21 | 22 | ```swift 23 | import SwiftUI 24 | import ExpandableText 25 | 26 | struct ExpandableText_Test: View { 27 | 28 | @State private var sampleText: String = "Do you think you're living an ordinary life? You are so mistaken it's difficult to even explain. The mere fact that you exist makes you extraordinary. The odds of you existing are less than winning the lottery, but here you are. Are you going to let this extraordinary opportunity pass?" 29 | 30 | var body: some View { 31 | ExpandableText(text: sampleText) 32 | .font(.body)//optional 33 | .foregroundColor(.primary)//optional 34 | .lineLimit(3)//optional 35 | .expandButton(TextSet(text: "more", font: .body, color: .blue))//optional 36 | .collapseButton(TextSet(text: "less", font: .body, color: .blue))//optional 37 | .expandAnimation(.easeOut)//optional 38 | .padding(.horizontal, 24)//optional 39 | } 40 | } 41 | ``` 42 | 43 | ## Modifier(optional) 44 | 45 | Modifier | Default 46 | --- | --- 47 | `.font(_ font: Font)` | `.body` 48 | `.lineLimit(_ lineLimit: Int)` | `3` 49 | `.foregroundColor(_ color: Color)` | `.primary` 50 | `.expandButton(_ expandButton: TextSet)` | `TextSet(text: "more", font: .body, color: .blue)` 51 | `.collapseButton(_ collapseButton: TextSet?)` | `nil(If it's nil, it doesn't show)` 52 | `.expandAnimation(_ animation: Animation?)` | `.none` 53 | 54 | 55 | -------------------------------------------------------------------------------- /Sources/ExpandableText/ExpandableText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableText.swift 3 | // 4 | // 5 | // Created by 이웅재 on 2021/10/12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct ExpandableText: View { 11 | var text : String 12 | 13 | @available(iOS 15, *) 14 | var markdownText: AttributedString { 15 | (try? AttributedString(markdown: text, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace))) ?? AttributedString() 16 | } 17 | 18 | var font: Font = .body 19 | var lineLimit: Int = 3 20 | var foregroundColor: Color = .primary 21 | 22 | var expandButton: TextSet = TextSet(text: "more", font: .body, color: .blue) 23 | var collapseButton: TextSet? = nil 24 | 25 | var animation: Animation? = .none 26 | 27 | @State private var expand : Bool = false 28 | @State private var truncated : Bool = false 29 | @State private var fullSize: CGFloat = 0 30 | 31 | public init(text: String) { 32 | self.text = text 33 | } 34 | public var body: some View { 35 | ZStack(alignment: .bottomTrailing){ 36 | Group { 37 | if #available(iOS 15.0, *) { 38 | Text(markdownText) 39 | } else { 40 | Text(text) 41 | } 42 | } 43 | .font(font) 44 | .foregroundColor(foregroundColor) 45 | .lineLimit(expand == true ? nil : lineLimit) 46 | .animation(animation, value: expand) 47 | .mask( 48 | VStack(spacing: 0){ 49 | Rectangle() 50 | .foregroundColor(.black) 51 | 52 | HStack(spacing: 0){ 53 | Rectangle() 54 | .foregroundColor(.black) 55 | if truncated{ 56 | if !expand { 57 | HStack(alignment: .bottom,spacing: 0){ 58 | LinearGradient( 59 | gradient: Gradient(stops: [ 60 | Gradient.Stop(color: .black, location: 0), 61 | Gradient.Stop(color: .clear, location: 0.8)]), 62 | startPoint: .leading, 63 | endPoint: .trailing) 64 | .frame(width: 32, height: expandButton.text.heightOfString(usingFont: fontToUIFont(font: expandButton.font))) 65 | 66 | Rectangle() 67 | .foregroundColor(.clear) 68 | .frame(width: expandButton.text.widthOfString(usingFont: fontToUIFont(font: expandButton.font)), alignment: .center) 69 | } 70 | } 71 | else if let collapseButton = collapseButton { 72 | HStack(alignment: .bottom,spacing: 0){ 73 | LinearGradient( 74 | gradient: Gradient(stops: [ 75 | Gradient.Stop(color: .black, location: 0), 76 | Gradient.Stop(color: .clear, location: 0.8)]), 77 | startPoint: .leading, 78 | endPoint: .trailing) 79 | .frame(width: 32, height: collapseButton.text.heightOfString(usingFont: fontToUIFont(font: collapseButton.font))) 80 | 81 | Rectangle() 82 | .foregroundColor(.clear) 83 | .frame(width: collapseButton.text.widthOfString(usingFont: fontToUIFont(font: collapseButton.font)), alignment: .center) 84 | } 85 | } 86 | } 87 | } 88 | .frame(height: expandButton.text.heightOfString(usingFont: fontToUIFont(font: font))) 89 | } 90 | ) 91 | 92 | if truncated { 93 | if let collapseButton = collapseButton { 94 | Button(action: { 95 | self.expand.toggle() 96 | }, label: { 97 | Text(expand == false ? expandButton.text : collapseButton.text) 98 | .font(expand == false ? expandButton.font : collapseButton.font) 99 | .foregroundColor(expand == false ? expandButton.color : collapseButton.color) 100 | }) 101 | } 102 | else if !expand { 103 | Button(action: { 104 | self.expand = true 105 | }, label: { 106 | Text(expandButton.text) 107 | .font(expandButton.font) 108 | .foregroundColor(expandButton.color) 109 | }) 110 | } 111 | } 112 | } 113 | .background( 114 | ZStack{ 115 | if !truncated { 116 | if fullSize != 0 { 117 | Text(text) 118 | .font(font) 119 | .lineLimit(lineLimit) 120 | .background( 121 | GeometryReader { geo in 122 | Color.clear 123 | .onAppear { 124 | if fullSize > geo.size.height { 125 | self.truncated = true 126 | print(geo.size.height) 127 | } 128 | } 129 | } 130 | ) 131 | } 132 | 133 | Text(text) 134 | .font(font) 135 | .lineLimit(999) 136 | .fixedSize(horizontal: false, vertical: true) 137 | .background(GeometryReader { geo in 138 | Color.clear 139 | .onAppear() { 140 | self.fullSize = geo.size.height 141 | } 142 | }) 143 | } 144 | } 145 | .hidden() 146 | ) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/ExpandableText/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // 4 | // 5 | // Created by 이웅재 on 2021/10/12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension ExpandableText { 11 | public func font(_ font: Font) -> ExpandableText { 12 | var result = self 13 | 14 | result.font = font 15 | 16 | return result 17 | } 18 | public func lineLimit(_ lineLimit: Int) -> ExpandableText { 19 | var result = self 20 | 21 | result.lineLimit = lineLimit 22 | return result 23 | } 24 | 25 | public func foregroundColor(_ color: Color) -> ExpandableText { 26 | var result = self 27 | 28 | result.foregroundColor = color 29 | return result 30 | } 31 | 32 | public func expandButton(_ expandButton: TextSet) -> ExpandableText { 33 | var result = self 34 | 35 | result.expandButton = expandButton 36 | return result 37 | } 38 | 39 | public func collapseButton(_ collapseButton: TextSet) -> ExpandableText { 40 | var result = self 41 | 42 | result.collapseButton = collapseButton 43 | return result 44 | } 45 | 46 | public func expandAnimation(_ animation: Animation?) -> ExpandableText { 47 | var result = self 48 | 49 | result.animation = animation 50 | return result 51 | } 52 | } 53 | 54 | extension String { 55 | func heightOfString(usingFont font: UIFont) -> CGFloat { 56 | let fontAttributes = [NSAttributedString.Key.font: font] 57 | let size = self.size(withAttributes: fontAttributes) 58 | return size.height 59 | } 60 | 61 | func widthOfString(usingFont font: UIFont) -> CGFloat { 62 | let fontAttributes = [NSAttributedString.Key.font: font] 63 | let size = self.size(withAttributes: fontAttributes) 64 | return size.width 65 | } 66 | } 67 | 68 | public struct TextSet { 69 | var text: String 70 | var font: Font 71 | var color: Color 72 | 73 | public init(text: String, font: Font, color: Color) { 74 | self.text = text 75 | self.font = font 76 | self.color = color 77 | } 78 | } 79 | 80 | func fontToUIFont(font: Font) -> UIFont { 81 | if #available(iOS 14.0, *) { 82 | switch font { 83 | case .largeTitle: 84 | return UIFont.preferredFont(forTextStyle: .largeTitle) 85 | case .title: 86 | return UIFont.preferredFont(forTextStyle: .title1) 87 | case .title2: 88 | return UIFont.preferredFont(forTextStyle: .title2) 89 | case .title3: 90 | return UIFont.preferredFont(forTextStyle: .title3) 91 | case .headline: 92 | return UIFont.preferredFont(forTextStyle: .headline) 93 | case .subheadline: 94 | return UIFont.preferredFont(forTextStyle: .subheadline) 95 | case .callout: 96 | return UIFont.preferredFont(forTextStyle: .callout) 97 | case .caption: 98 | return UIFont.preferredFont(forTextStyle: .caption1) 99 | case .caption2: 100 | return UIFont.preferredFont(forTextStyle: .caption2) 101 | case .footnote: 102 | return UIFont.preferredFont(forTextStyle: .footnote) 103 | case .body: 104 | return UIFont.preferredFont(forTextStyle: .body) 105 | default: 106 | return UIFont.preferredFont(forTextStyle: .body) 107 | } 108 | } else { 109 | switch font { 110 | case .largeTitle: 111 | return UIFont.preferredFont(forTextStyle: .largeTitle) 112 | case .title: 113 | return UIFont.preferredFont(forTextStyle: .title1) 114 | // case .title2: 115 | // return UIFont.preferredFont(forTextStyle: .title2) 116 | // case .title3: 117 | // return UIFont.preferredFont(forTextStyle: .title3) 118 | case .headline: 119 | return UIFont.preferredFont(forTextStyle: .headline) 120 | case .subheadline: 121 | return UIFont.preferredFont(forTextStyle: .subheadline) 122 | case .callout: 123 | return UIFont.preferredFont(forTextStyle: .callout) 124 | case .caption: 125 | return UIFont.preferredFont(forTextStyle: .caption1) 126 | // case .caption2: 127 | // return UIFont.preferredFont(forTextStyle: .caption2) 128 | case .footnote: 129 | return UIFont.preferredFont(forTextStyle: .footnote) 130 | case .body: 131 | return UIFont.preferredFont(forTextStyle: .body) 132 | default: 133 | return UIFont.preferredFont(forTextStyle: .body) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Tests/ExpandableTextTests/ExpandableTextTests.swift: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------