Compare commits

...

60 commits

Author SHA1 Message Date
Woit 9b91add453 wip 2024-12-16 08:00:23 +01:00
Woit 413d4f1c72 wip 2024-12-09 20:49:39 +01:00
Woit 55b3469284 wip 2024-12-09 20:28:46 +01:00
Woit 868f326bbc wip 2024-12-09 19:53:56 +01:00
Woit edfc3785e9 wip 2024-12-09 19:40:05 +01:00
Woit ae767456dd wip 2024-12-09 19:25:52 +01:00
Woit bfb7bd00aa wip 2024-12-09 18:02:49 +01:00
Woit cc2207ba82 wip 2024-12-07 00:59:46 +01:00
Woit b4a708ffad wip 2024-12-07 00:09:49 +01:00
Woit cbe67fe3f1 wip 2024-12-06 23:04:25 +01:00
Woit 4378b2806c wip 2024-12-06 22:47:00 +01:00
Woit 8515b79b57 wip 2024-12-06 22:43:50 +01:00
Woit e6b3d856a7 wip 2024-12-06 22:32:17 +01:00
Woit 02801e46b4 wip 2024-12-06 20:32:41 +01:00
Woit 8e3bc01632 wip 2024-12-06 13:35:22 +01:00
Woit 406b2fde1d wip 2024-12-06 12:56:09 +01:00
Woit c8cf403d15 wip 2024-12-06 11:48:06 +01:00
Woit 006ae2a612 wip 2024-12-05 18:14:22 +01:00
Woit f8030ed60e wip 2024-12-04 17:46:16 +01:00
Woit f489425b99 wip 2024-12-04 16:00:37 +01:00
Woit 6a4acfb6f3 wip 2024-12-02 16:50:23 +01:00
Woit f8ce451b63 wip 2024-11-30 00:41:01 +01:00
Woit 880408b04a wip 2024-11-29 23:36:19 +01:00
Woit 6934b0fa97 wip 2024-11-29 23:18:57 +01:00
Woit eb037c452a wip 2024-11-29 18:43:27 +01:00
Woit 87642a5a10 wip 2024-11-29 18:35:20 +01:00
Woit 32e42f8506 wip 2024-11-29 17:20:58 +01:00
Woit c5750f9825 wip 2024-11-29 16:07:28 +01:00
Woit 340bb5a1b4 wip 2024-11-28 18:07:22 +01:00
Woit 598b07661c wip 2024-11-28 18:04:59 +01:00
Woit ca5e4b930c wip 2024-11-28 17:34:41 +01:00
Woit dce50cddca wip 2024-11-28 16:50:59 +01:00
Woit 515ed2197e wip 2024-11-28 16:46:16 +01:00
Woit 2b876e5ae6 wip 2024-11-27 16:49:36 +01:00
Woit 9b8b5d1ae6 wip 2024-11-25 16:44:05 +01:00
Woit 60e14a5406 wip 2024-11-25 16:26:52 +01:00
Woit 13ace49560 wip 2024-11-25 13:02:36 +01:00
Woit a59d3bbeb7 wip 2024-11-25 12:49:47 +01:00
Woit 477e58f35c wip 2024-11-25 11:51:00 +01:00
Woit ea333acb1a wip 2024-11-24 19:42:46 +01:00
Woit 9d6f610b63 add conversation screens 2024-11-24 00:22:11 +01:00
Woit 57793daf3d wip 2024-11-23 17:23:56 +01:00
Woit c4d70ef77d wip 2024-11-23 17:23:49 +01:00
Woit 9e07aed5fd wip 2024-11-22 17:54:07 +01:00
Woit 49f1028d69 wip 2024-11-22 17:34:56 +01:00
Woit 4c42b21559 wip 2024-11-22 15:45:38 +01:00
Woit d1e3bc54bf wip 2024-11-22 15:06:06 +01:00
Woit ad7a2b4f59 wip 2024-11-21 17:03:55 +01:00
Woit 22b6f415d2 wip 2024-11-21 14:32:42 +01:00
Woit ee2f68e33b wip 2024-11-20 20:51:05 +01:00
Woit 3640f0ec06 fix weird crash 2024-11-20 17:37:10 +01:00
Woit e1f7971ee7 wip 2024-11-20 14:31:51 +01:00
Woit e210861382 move prev sources 2024-11-19 17:07:51 +01:00
Woit f06208ec66 wip 2024-11-19 16:42:36 +01:00
Woit f9de9e3800 add swiftgen 2024-11-19 15:14:08 +01:00
Woit a808af8057 add swiftlint 2024-11-19 15:05:27 +01:00
Woit cc7a3b9286 wip 2024-11-19 14:15:08 +01:00
Woit 98061025f3 wip 2024-11-19 13:59:22 +01:00
Woit 4061484298 wip 2024-11-19 13:48:58 +01:00
Woit 10475f9a72 copy monal src 2024-11-18 15:53:52 +01:00
754 changed files with 105870 additions and 116 deletions

176
.gitignore vendored
View file

@ -1,123 +1,77 @@
docs
Frameworks
#########################
# **.gitignore** file for Xcode4 / OS X Source projects
#
# NB: if you are storing "built" products, this WILL NOT WORK,
# and you should use a different **.gitignore** (or none at all)
# This file is for SOURCE projects, where there are many extra
# files that we want to exclude
#
# For updates, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects
#########################
# rust bridge
rust/monal-rust-swift-bridge/generated
rust/LibMonalRustSwiftBridge
#####
# OS X temporary files that should never be committed
# Created by https://www.toptal.com/developers/gitignore/api/rust
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
!rust/Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# End of https://www.toptal.com/developers/gitignore/api/rust
# macOS
.DS_Store
*.swp
profile
# Xcode
Monal/Monal.xcodeproj/xcuserdata/*
Monal/Monal.xcodeproj/project.xcworkspace/xcshareddata/*
Monal/Monal.xcodeproj/project.xcworkspace/xcuserdata/*
contents.xcworkspacedata
._*
*.xcscmblueprint
*.xccheckout
Monal/Monal.xcworkspace/xcshareddata/*
Monal/Monal.xcworkspace/xcuserdata/*
####
# Xcode temporary files that should never be committed
#
# NB: NIB/XIB files still exist even on Storyboard projects, so we want this...
*~.nib
####
# Xcode build files -
#
# NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData"
DerivedData/
# NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build"
build/
#####
# Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups)
#
# This is complicated:
#
# SOMETIMES you need to put this file in version control.
# Apple designed it poorly - if you use "custom executables", they are
# saved in this file.
# 99% of projects do NOT use those, so they do NOT want to version control this file.
# ..but if you're in the 1%, comment out the line "*.pbxuser"
Monal/build/*
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
# NB: also, whitelist the default ones, some projects need to use these
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
####
# Xcode 4 - semi-personal settings, often included in workspaces
#
# You can safely ignore the xcuserdata files - but do NOT ignore the files next to them
#
xcuserdata
####
# XCode 4 workspaces - more detailed
#
# Workspaces are important! They are a core feature of Xcode - don't exclude them :)
#
# Workspace layout is quite spammy. For reference:
#
# (root)/
# (project-name).xcodeproj/
# project.pbxproj
# project.xcworkspace/
# contents.xcworkspacedata
# xcuserdata/
# (your name)/xcuserdatad/
# xcuserdata/
# (your name)/xcuserdatad/
#
#
#
# Xcode 4 workspaces - SHARED
#
# This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results
# But if you're going to kill personal workspaces, at least keep the shared ones...
#
#
!xcshareddata
####
# XCode 4 build-schemes
#
# PRIVATE ones are stored inside xcuserdata
!xcschemes
####
# Xcode 4 - Deprecated classes
#
# Allegedly, if you manually "deprecate" your classes, they get moved here.
#
# We're using source-control, so this is a "feature" that we do not want!
profile
*.moved-aside
/.idea
/ConversationsClassic/.idea
/ConversationsClassic.xcodeproj
/Info.plist
/ConversationsClassic/ConversationsClassic.entitlements
/XMPPSwift/Client/VoIP/rickroll.mp4
/.nvim
/buildServer.json
TODO.txt
PASSWD.txt
DerivedData
#
Monal/.nvim
Monal/buildServer.json
Monal/MY_MARKS.txt
# Pods
Monal/Pods
#Don't accidentally commit localization state
Monal/localization/external
Monal/localization/external/*
Monal/shareSheet-iOS/localization/external
Monal/shareSheet-iOS/localization/external/*
# certs and other encrypted stuff
*.pem
*.key
*.csr
*.cer
*.mobileprovision
*.provisionprofile
*.p12
Monal/Classes/secrets.h

125
.swiftlint.yml Normal file
View file

@ -0,0 +1,125 @@
---
colon:
severity: error
line_length:
ignores_comments: true
warning: 260
error: 300
type_body_length:
warning: 300
error: 500
file_length:
warning: 800
error: 1000
function_parameter_count:
warning: 20
error: 30
function_body_length:
warning: 120
error: 150
cyclomatic_complexity:
warning: 40
error: 50
nesting:
type_level:
warning: 3
error: 6
function_level:
warning: 500
error: 10
vertical_parameter_alignment:
severity: warning
implicitly_unwrapped_optional:
severity: warning
force_unwrapping:
severity: error
vertical_whitespace:
severity: error
force_try:
severity: error
trailing_semicolon:
severity: error
type_name:
min_length:
warninig: 3
error: 0
max_length:
warninig: 40
error: 80
identifier_name:
min_length: 3
max_length: 60
# validates_start_with_lowercase: true
allowed_symbols: "_"
excluded:
- iv
- id
- ip
- on
- ui
- x
- y
- tz
- to
- db
- _db
# Disable rules from the default enabled set.
disabled_rules:
- trailing_whitespace
- implicit_getter
- redundant_string_enum_value
- switch_case_alignment
# Enable rules not from the default set.
opt_in_rules:
# - function_default_parameter_at_end
- empty_count
- indentation_width
# - index_at_zero
- legacy_constant
# - implicitly_unwrapped_optional
- force_unwrapping
# - no header
- file_header
# - for force unwrapping
- implicitly_unwrapped_optional
- vertical_parameter_alignment_on_call
- vertical_whitespace_between_cases
- vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces
# Acts as a whitelist, only the rules specified in this list will be enabled. Can not be specified alongside disabled_rules or opt_in_rules.
only_rules:
# This is an entirely separate list of rules that are only run by the analyze command. All analyzer rules are opt-in, so this is the only configurable rule list (there is no disabled/whitelist equivalent).
analyzer_rules:
- unused_import
- unused_declaration
unused_declaration:
include_public_and_open: true
# paths to ignore during linting. Takes precedence over `included`.
excluded:
- .swiftgen
- "**/Generated"
- Classes
- Pods
- MonalUITests
- MonalXMPPUnitTests

BIN
Art/alpha_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
Art/callkit_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
Art/chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
Art/chat2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
Art/chat_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
Art/friends.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
Art/friends2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
Art/friends_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
Art/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

149
Art/monal.svg Normal file
View file

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="20mm"
height="20.085215mm"
viewBox="0 0 20 20.085215"
version="1.1"
id="svg3834"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="monal.svg"
style="enable-background:new"
inkscape:export-filename="/Users/anurodhp/Desktop/monal.png"
inkscape:export-xdpi="1300.48"
inkscape:export-ydpi="1300.48">
<defs
id="defs3828">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 10.042608 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="20 : 10.042608 : 1"
inkscape:persp3d-origin="10 : 6.6950717 : 1"
id="perspective841" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="9.0725624"
inkscape:cx="28.787632"
inkscape:cy="44.279616"
inkscape:document-units="mm"
inkscape:current-layer="layer3"
showgrid="false"
inkscape:window-width="1280"
inkscape:window-height="700"
inkscape:window-x="-4"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:pagecheckerboard="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata3831">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-181.15717,-221.0978)"
style="display:inline">
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 2"
style="display:inline">
<rect
id="rect30"
width="20"
height="20"
x="181.15717"
y="221.18301"
style="stroke-width:0.26458332" />
<rect
style="fill:#2cd3e3;fill-opacity:1;stroke-width:0.26458332;opacity:1"
id="rect48"
width="20"
height="20"
x="181.15717"
y="221.18301"
inkscape:export-xdpi="1299.6801"
inkscape:export-ydpi="1299.6801" />
</g>
<g
id="g25"
transform="matrix(1.25,0,0,1.25,-45.477532,-55.597438)">
<g
id="g4578"
transform="translate(0.8477441,0.34537723)">
<path
style="fill:#ffffff;fill-opacity:1;stroke-width:0.21166666"
d="m 191.56803,221.43167 c 0.44531,-0.54927 1.00283,-1.55943 1.42657,-1.47418 -1.6117,3.28945 -5.09363,5.55983 -6.77643,8.41276 -1.48053,1.02259 -5.59221,4.97552 -3.50788,4.62647 2.55426,-0.33289 4.26942,-0.18434 6.47037,-0.0863 2.97857,0.44913 2.99008,0.35922 1.42237,-0.94939 -4.08407,-2.92741 -7.3886,-5.96422 -2.55345,-2.51434 1.19608,0.89899 3.58977,2.88011 4.62961,3.96256 0.14192,0.38081 -1.36875,-1.03083 -1.63269,-1.38151 1.00787,0.47021 4.41857,3.94275 5.65285,3.45204 1.11708,-0.36357 1.5434,-1.4884 2.64165,-0.91497 0.73976,-0.52896 -0.81203,-1.84714 0.109,-2.45498 1.18118,-0.18435 -0.2902,-1.51785 0.93686,-1.87197 0.72674,-0.62762 -1.09372,-1.01894 -1.52077,-1.45287 -0.89602,-0.45402 -0.35512,-1.55824 -0.004,-1.79086 0.0846,-1.03756 1.78245,-0.86434 2.36976,-1.66716 0.74024,-0.20999 1.24713,-1.13045 0.10892,-1.05465 -1.29104,-0.33585 -2.48308,0.3858 -3.65095,0.80824 -0.75744,0.43619 -1.51475,-0.3745 -0.67602,-0.9122 0.086,-0.87983 -0.49258,-4.71845 2.78132,-3.06301 0.34122,0.76448 -1.29924,0.73222 -1.56848,1.44997 -0.51725,0.58889 -0.82268,1.36738 -0.81609,2.1534 1.37222,-0.61392 1.59347,-2.39848 2.89681,-3.09724 0.62326,-0.9785 1.02124,0.86222 0.0967,0.71688 -0.41512,0.46396 -1.82667,1.32778 -1.43703,1.64655 0.99965,-0.74709 2.16479,-0.20775 3.17351,0.0358 1.13327,0.97208 -0.7695,1.66177 -1.40739,2.19644 -0.72103,0.3951 -1.48929,0.73257 -1.53088,1.60897 -0.81354,-0.32254 0.12422,1.14184 0.65017,1.27543 0.43315,0.20734 0.91104,0.44698 0.50741,-0.12798 0.30174,-0.53571 1.10164,0.28036 0.4594,-0.4453 0.0479,-1.20018 1.96828,-0.76448 2.64659,-1.55994 1.00428,-0.71951 -0.0847,-1.22662 -0.85018,-1.0046 -0.50182,0.06 -1.07873,-0.43882 -0.22366,-0.39397 0.83328,-0.14122 2.91477,0.25377 1.94632,1.4131 -1.06336,0.52011 -1.39764,1.49535 -1.68824,2.5366 -0.64436,0.88909 0.0567,1.65993 0.51433,2.21278 -0.0755,1.02463 -0.9696,1.82839 -1.60005,2.58943 -0.34214,0.34256 -1.17179,0.99352 -0.4029,0.26644 0.9274,-1.03713 1.17061,-2.97085 -0.091,-3.84667 -0.84429,-0.40075 -0.98573,0.46872 -0.61694,1.00935 -0.25867,0.8 -0.87545,1.54176 -0.22698,2.35526 -0.13692,1.16852 -1.3052,0.0978 -1.98479,0.71485 -0.94554,0.26803 -0.56905,0.54435 -1.5424,0.66888 1.68588,0.55388 2.10672,-0.0925 3.78947,-0.34726 -0.64558,0.75646 -1.80506,1.10898 -2.81213,1.13009 -2.03076,-0.002 -3.80891,-1.10663 -5.61502,-1.89118 -1.12924,-0.54753 -4.82644,-1.3203 -6.13091,-1.1211 -1.74005,0.0829 -1.83717,0.0547 -3.444,0.29301 -1.26438,-0.004 -1.42104,-0.75538 -0.51486,-2.02424 3.57949,-3.11534 6.69133,-7.03177 9.59613,-10.08739 z m -1.14004,8.46391 c -6.64532,4.78894 -3.32266,2.39447 0,0 z m -1.20322,0.0301 c -1.1412,-0.63874 -0.31307,-2.07894 0.17279,-2.89173 0.89169,-1.36972 2.86985,-1.39236 4.16156,-0.61159 1.54938,1.0298 2.07366,1.03412 2.06974,1.79612 -1.4368,0.34261 -2.55154,1.41885 -3.96896,1.80186 -0.79909,0.16878 -1.64355,0.0662 -2.43562,-0.0947 z m 3.57167,-0.77942 c 1.35312,-0.0504 0.6421,-1.87946 -0.27756,-2.06232 -2.33109,-0.63103 -2.8492,0.97005 -3.34013,1.46545 0.0455,1.37625 2.35919,1.3962 3.29693,0.77529 0.10657,-0.0601 -0.10797,0.0576 0,0 z m -3.61769,-0.59687 c -5.81249,5.68643 -2.90624,2.84321 0,0 z m 4.87351,-8.29404 c -9.0615,11.21579 -4.53075,5.60789 0,0 z"
id="path3882"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" />
</g>
</g>
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
id="path4583"
cx="185.16042"
cy="234.62871"
rx="0.74570084"
ry="0.51021636" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 3"
style="display:inline">
<path
style="opacity:1;vector-effect:none;fill:#2cd3e3;fill-opacity:1;fill-rule:evenodd;stroke-width:0.43717846;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path884"
sodipodi:type="arc"
sodipodi:cx="7.5222836"
sodipodi:cy="13.490612"
sodipodi:rx="3.1455584"
sodipodi:ry="1.9101746"
sodipodi:start="0"
sodipodi:end="6.1045858"
sodipodi:open="true"
d="M 10.667842,13.490612 A 3.1455584,1.9101746 0 0 1 7.6626857,15.398883 3.1455584,1.9101746 0 0 1 4.3892589,13.660964 3.1455584,1.9101746 0 0 1 7.102196,11.597548 3.1455584,1.9101746 0 0 1 10.617807,13.151267"
transform="matrix(0.93916761,-0.34345916,0.51106856,0.85953995,0,0)" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
style="display:inline">
<circle
style="opacity:1;vector-effect:none;fill:#b3ff80;fill-opacity:1;fill-rule:evenodd;stroke-width:0.38550848;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path907"
cx="13.877442"
cy="8.9755268"
r="1.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
Art/park_black_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

BIN
Art/park_colors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

BIN
Art/park_white_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

29
LICENSE Normal file
View file

@ -0,0 +1,29 @@
BSD 2-Clause License
Copyright (c) 2024, Thilo Molitor
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of Anurodh Pokharel

27
Monal/.bartycrouch.toml Normal file
View file

@ -0,0 +1,27 @@
[update]
tasks = ["interfaces", "code", "normalize"]
[update.interfaces]
paths = ["."]
defaultToBase = true
ignoreEmptyStrings = true
unstripped = false
[update.code]
codePaths = ["Classes", "shareSheet-iOS", "NotificationService"]
localizablePaths = ["localization", "shareSheet-iOS/localization"]
defaultToKeys = true
additive = true
unstripped = false
plistArguments = true
[update.normalize]
paths = ["."]
sourceLocale = "base"
harmonizeWithSource = true
sortByKeys = true
[lint]
paths = ["."]
duplicateKeys = true
emptyValues = true

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string></string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>SwiftGen_SwiftGenCLI</string>
<key>CFBundleIdentifier</key>
<string>SwiftGen.SwiftGenCLI.resources</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>SwiftGen_SwiftGenCLI</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>13A233</string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>11.3</string>
<key>DTSDKBuild</key>
<string>20E214</string>
<key>DTSDKName</key>
<string>macosx11.3</string>
<key>DTXcode</key>
<string>1300</string>
<key>DTXcodeBuild</key>
<string>13A233</string>
<key>LSMinimumSystemVersion</key>
<string>10.11</string>
</dict>
</plist>

View file

@ -0,0 +1,43 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if palettes %}
{% set enumName %}{{param.enumName|default:"ColorName"}}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
#if os(macOS)
import AppKit
{% if enumName != 'NSColor' %}{{accessModifier}} enum {{enumName}} { }{% endif %}
#elseif os(iOS) || os(tvOS) || os(watchOS)
import UIKit
{% if enumName != 'UIColor' %}{{accessModifier}} enum {{enumName}} { }{% endif %}
#endif
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Colors
// swiftlint:disable identifier_name line_length type_body_length
{{accessModifier}} extension {{enumName}} {
{% macro h2f hex %}{{hex|hexToInt|int255toFloat}}{% endmacro %}
{% macro enumBlock colors accessPrefix %}
{% for color in colors %}
/// 0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}} (r: {{color.red|hexToInt}}, g: {{color.green|hexToInt}}, b: {{color.blue|hexToInt}}, a: {{color.alpha|hexToInt}})
{{accessPrefix}}static let {{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = #colorLiteral(red: {% call h2f color.red %}, green: {% call h2f color.green %}, blue: {% call h2f color.blue %}, alpha: {% call h2f color.alpha %})
{% endfor %}
{% endmacro %}
{% if palettes.count > 1 or param.forceFileNameEnum %}
{% set accessPrefix %}{{accessModifier}} {% endset %}
{% for palette in palettes %}
enum {{palette.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call enumBlock palette.colors accessPrefix %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call enumBlock palettes.first.colors "" %}
{% endif %}
}
// swiftlint:enable identifier_name line_length type_body_length
{% else %}
// No color found
{% endif %}

View file

@ -0,0 +1,43 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if palettes %}
{% set enumName %}{{param.enumName|default:"ColorName"}}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
#if os(macOS)
import AppKit
{% if enumName != 'NSColor' %}{{accessModifier}} enum {{enumName}} { }{% endif %}
#elseif os(iOS) || os(tvOS) || os(watchOS)
import UIKit
{% if enumName != 'UIColor' %}{{accessModifier}} enum {{enumName}} { }{% endif %}
#endif
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Colors
// swiftlint:disable identifier_name line_length type_body_length
{{accessModifier}} extension {{enumName}} {
{% macro h2f hex %}{{hex|hexToInt|int255toFloat}}{% endmacro %}
{% macro enumBlock colors accessPrefix %}
{% for color in colors %}
/// 0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}} (r: {{color.red|hexToInt}}, g: {{color.green|hexToInt}}, b: {{color.blue|hexToInt}}, a: {{color.alpha|hexToInt}})
{{accessPrefix}}static let {{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = #colorLiteral(red: {% call h2f color.red %}, green: {% call h2f color.green %}, blue: {% call h2f color.blue %}, alpha: {% call h2f color.alpha %})
{% endfor %}
{% endmacro %}
{% if palettes.count > 1 or param.forceFileNameEnum %}
{% set accessPrefix %}{{accessModifier}} {% endset %}
{% for palette in palettes %}
enum {{palette.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call enumBlock palette.colors accessPrefix %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call enumBlock palettes.first.colors "" %}
{% endif %}
}
// swiftlint:enable identifier_name line_length type_body_length
{% else %}
// No color found
{% endif %}

View file

@ -0,0 +1,84 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if palettes %}
{% set colorAlias %}{{param.colorAliasName|default:"Color"}}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
#if os(macOS)
import AppKit.NSColor
{{accessModifier}} typealias {{colorAlias}} = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
import UIKit.UIColor
{{accessModifier}} typealias {{colorAlias}} = UIColor
#endif
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Colors
// swiftlint:disable identifier_name line_length type_body_length
{% set enumName %}{{param.enumName|default:"ColorName"}}{% endset %}
{{accessModifier}} struct {{enumName}} {
{{accessModifier}} let rgbaValue: UInt32
{{accessModifier}} var color: {{colorAlias}} { return {{colorAlias}}(named: self) }
{% macro rgbaValue color %}0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}}{% endmacro %}
{% macro enumBlock colors %}
{% for color in colors %}
/// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#{{color.red}}{{color.green}}{{color.blue}}"></span>
/// Alpha: {{color.alpha|hexToInt|int255toFloat|percent}} <br/> (0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}})
{{accessModifier}} static let {{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}(rgbaValue: {% call rgbaValue color %})
{% endfor %}
{% endmacro %}
{% if palettes.count > 1 or param.forceFileNameEnum %}
{% for palette in palettes %}
{{accessModifier}} enum {{palette.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call enumBlock palette.colors %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call enumBlock palettes.first.colors %}
{% endif %}
}
// swiftlint:enable identifier_name line_length type_body_length
// MARK: - Implementation Details
internal extension {{colorAlias}} {
convenience init(rgbaValue: UInt32) {
let components = RGBAComponents(rgbaValue: rgbaValue).normalized
self.init(red: components[0], green: components[1], blue: components[2], alpha: components[3])
}
}
private struct RGBAComponents {
let rgbaValue: UInt32
private var shifts: [UInt32] {
[
rgbaValue >> 24, // red
rgbaValue >> 16, // green
rgbaValue >> 8, // blue
rgbaValue // alpha
]
}
private var components: [CGFloat] {
shifts.map {
CGFloat($0 & 0xff)
}
}
var normalized: [CGFloat] {
components.map { $0 / 255.0 }
}
}
{{accessModifier}} extension {{colorAlias}} {
convenience init(named color: {{enumName}}) {
self.init(rgbaValue: color.rgbaValue)
}
}
{% else %}
// No color found
{% endif %}

View file

@ -0,0 +1,84 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if palettes %}
{% set colorAlias %}{{param.colorAliasName|default:"Color"}}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
#if os(macOS)
import AppKit.NSColor
{{accessModifier}} typealias {{colorAlias}} = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
import UIKit.UIColor
{{accessModifier}} typealias {{colorAlias}} = UIColor
#endif
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Colors
// swiftlint:disable identifier_name line_length type_body_length
{% set enumName %}{{param.enumName|default:"ColorName"}}{% endset %}
{{accessModifier}} struct {{enumName}} {
{{accessModifier}} let rgbaValue: UInt32
{{accessModifier}} var color: {{colorAlias}} { return {{colorAlias}}(named: self) }
{% macro rgbaValue color %}0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}}{% endmacro %}
{% macro enumBlock colors %}
{% for color in colors %}
/// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#{{color.red}}{{color.green}}{{color.blue}}"></span>
/// Alpha: {{color.alpha|hexToInt|int255toFloat|percent}} <br/> (0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}})
{{accessModifier}} static let {{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}(rgbaValue: {% call rgbaValue color %})
{% endfor %}
{% endmacro %}
{% if palettes.count > 1 or param.forceFileNameEnum %}
{% for palette in palettes %}
{{accessModifier}} enum {{palette.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call enumBlock palette.colors %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call enumBlock palettes.first.colors %}
{% endif %}
}
// swiftlint:enable identifier_name line_length type_body_length
// MARK: - Implementation Details
internal extension {{colorAlias}} {
convenience init(rgbaValue: UInt32) {
let components = RGBAComponents(rgbaValue: rgbaValue).normalized
self.init(red: components[0], green: components[1], blue: components[2], alpha: components[3])
}
}
private struct RGBAComponents {
let rgbaValue: UInt32
private var shifts: [UInt32] {
[
rgbaValue >> 24, // red
rgbaValue >> 16, // green
rgbaValue >> 8, // blue
rgbaValue // alpha
]
}
private var components: [CGFloat] {
shifts.map {
CGFloat($0 & 0xff)
}
}
var normalized: [CGFloat] {
components.map { $0 / 255.0 }
}
}
{{accessModifier}} extension {{colorAlias}} {
convenience init(named color: {{enumName}}) {
self.init(rgbaValue: color.rgbaValue)
}
}
{% else %}
// No color found
{% endif %}

View file

@ -0,0 +1,211 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
// swiftlint:disable superfluous_disable_command implicit_return
// swiftlint:disable sorted_imports
import CoreData
import Foundation
{% for import in param.extraImports %}
import {{ import }}
{% empty %}
{# If extraImports is a single String instead of an array, `for` considers it empty but we still have to check if there's a single String value #}
{% if param.extraImports %}import {{ param.extraImports }}{% endif %}
{% endfor %}
// swiftlint:disable attributes file_length vertical_whitespace_closing_braces
// swiftlint:disable identifier_name line_length type_body_length
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% for model in models %}
{% for name, entity in model.entities %}
{% set superclass %}{{ model.entities[entity.superEntity].className|default:"NSManagedObject" }}{% endset %}
{% set entityClassName %}{{ entity.className|default:"NSManagedObject" }}{% endset %}
// MARK: - {{ entity.name }}
{% if not entity.shouldGenerateCode %}
// Note: '{{ entity.name }}' has codegen enabled for Xcode, skipping code generation.
{% elif entityClassName|contains:"." %}
// Warning: '{{ entityClassName }}' cannot be a valid type name, skipping code generation.
{% else %}
{% if param.generateObjcName %}
@objc({{ entityClassName }})
{% endif %}
{{ accessModifier }} class {{ entityClassName }}: {{ superclass }} {
{% set override %}{% if superclass != "NSManagedObject" %}override {% endif %}{% endset %}
{{ override }}{{ accessModifier }} class var entityName: String {
return "{{ entity.name }}"
}
{{ override }}{{ accessModifier }} class func entity(in managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
return NSEntityDescription.entity(forEntityName: entityName, in: managedObjectContext)
}
@available(*, deprecated, renamed: "makeFetchRequest", message: "To avoid collisions with the less concrete method in `NSManagedObject`, please use `makeFetchRequest()` instead.")
@nonobjc {{ accessModifier }} class func fetchRequest() -> NSFetchRequest<{{ entityClassName }}> {
return NSFetchRequest<{{ entityClassName }}>(entityName: entityName)
}
@nonobjc {{ accessModifier }} class func makeFetchRequest() -> NSFetchRequest<{{ entityClassName }}> {
return NSFetchRequest<{{ entityClassName }}>(entityName: entityName)
}
// swiftlint:disable discouraged_optional_boolean discouraged_optional_collection
{% for attribute in entity.attributes %}
{% if attribute.userInfo.RawType %}
{% set rawType attribute.userInfo.RawType %}
{% set unwrapOptional attribute.userInfo.unwrapOptional %}
{{ accessModifier }} var {{ attribute.name }}: {{ rawType }}{% if not unwrapOptional %}?{% endif %} {
get {
let key = "{{ attribute.name }}"
willAccessValue(forKey: key)
defer { didAccessValue(forKey: key) }
{% if unwrapOptional %}
guard let value = primitiveValue(forKey: key) as? {{ rawType }}.RawValue,
let result = {{ rawType }}(rawValue: value) else {
fatalError("Could not convert value for key '\(key)' to type '{{ rawType }}'")
}
return result
{% else %}
guard let value = primitiveValue(forKey: key) as? {{ rawType }}.RawValue else {
return nil
}
return {{ rawType }}(rawValue: value)
{% endif %}
}
set {
let key = "{{ attribute.name }}"
willChangeValue(forKey: key)
defer { didChangeValue(forKey: key) }
setPrimitiveValue(newValue{% if not unwrapOptional %}?{% endif %}.rawValue, forKey: key)
}
}
{% elif attribute.usesScalarValueType and attribute.isOptional %}
{{ accessModifier }} var {{ attribute.name }}: {{ attribute.typeName }}? {
get {
let key = "{{ attribute.name }}"
willAccessValue(forKey: key)
defer { didAccessValue(forKey: key) }
return primitiveValue(forKey: key) as? {{ attribute.typeName }}
}
set {
let key = "{{ attribute.name }}"
willChangeValue(forKey: key)
defer { didChangeValue(forKey: key) }
setPrimitiveValue(newValue, forKey: key)
}
}
{% else %}
@NSManaged {{ accessModifier }} var {{ attribute.name }}: {{ attribute.typeName }}{% if attribute.isOptional %}?{% endif %}
{% endif %}
{% endfor %}
{% for relationship in entity.relationships %}
{% if relationship.isToMany %}
@NSManaged {{ accessModifier }} var {{ relationship.name }}: {% if relationship.isOrdered %}NSOrderedSet{% else %}Set<{{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}>{% endif %}{% if relationship.isOptional %}?{% endif %}
{% else %}
@NSManaged {{ accessModifier }} var {{ relationship.name }}: {{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}{% if relationship.isOptional %}?{% endif %}
{% endif %}
{% endfor %}
{% for fetchedProperty in entity.fetchedProperties %}
@NSManaged {{ accessModifier }} var {{ fetchedProperty.name }}: [{{ fetchedProperty.fetchRequest.entity }}]
{% endfor %}
// swiftlint:enable discouraged_optional_boolean discouraged_optional_collection
}
{% for relationship in entity.relationships where relationship.isToMany %}
{% set destinationEntityClassName %}{{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}{% endset %}
{% set collectionClassName %}{% if relationship.isOrdered %}NSOrderedSet{% else %}Set<{{ destinationEntityClassName }}>{% endif %}{% endset %}
{% set relationshipName %}{{ relationship.name | upperFirstLetter }}{% endset %}
// MARK: Relationship {{ relationshipName }}
extension {{ entityClassName }} {
{% if relationship.isOrdered %}
@objc(insertObject:in{{ relationshipName }}AtIndex:)
@NSManaged public func insertInto{{ relationshipName }}(_ value: {{ destinationEntityClassName }}, at idx: Int)
@objc(removeObjectFrom{{ relationshipName }}AtIndex:)
@NSManaged public func removeFrom{{ relationshipName }}(at idx: Int)
@objc(insert{{ relationshipName }}:atIndexes:)
@NSManaged public func insertInto{{ relationshipName }}(_ values: [{{ destinationEntityClassName }}], at indexes: NSIndexSet)
@objc(remove{{ relationshipName }}AtIndexes:)
@NSManaged public func removeFrom{{ relationshipName }}(at indexes: NSIndexSet)
@objc(replaceObjectIn{{ relationshipName }}AtIndex:withObject:)
@NSManaged public func replace{{ relationshipName }}(at idx: Int, with value: {{ destinationEntityClassName }})
@objc(replace{{ relationshipName }}AtIndexes:with{{ relationshipName }}:)
@NSManaged public func replace{{ relationshipName }}(at indexes: NSIndexSet, with values: [{{ destinationEntityClassName }}])
{% endif %}
@objc(add{{ relationshipName }}Object:)
@NSManaged public func addTo{{ relationshipName }}(_ value: {{ destinationEntityClassName }})
@objc(remove{{ relationshipName }}Object:)
@NSManaged public func removeFrom{{ relationshipName }}(_ value: {{ destinationEntityClassName }})
@objc(add{{ relationshipName }}:)
@NSManaged public func addTo{{ relationshipName }}(_ values: {{ collectionClassName }})
@objc(remove{{ relationshipName }}:)
@NSManaged public func removeFrom{{ relationshipName }}(_ values: {{ collectionClassName }})
}
{% endfor %}
{% if model.fetchRequests[entity.name].count > 0 %}
// MARK: Fetch Requests
extension {{ entityClassName }} {
{% for fetchRequest in model.fetchRequests[entity.name] %}
{% set resultTypeName %}{% filter removeNewlines:"leading" %}
{% if fetchRequest.resultType == "Object" %}
{{ entityClassName }}
{% elif fetchRequest.resultType == "Object ID" %}
NSManagedObjectID
{% elif fetchRequest.resultType == "Dictionary" %}
[String: Any]
{% endif %}
{% endfilter %}{% endset %}
class func fetch{{ fetchRequest.name | upperFirstLetter }}({% filter removeNewlines:"leading" %}
managedObjectContext: NSManagedObjectContext
{% for variableName, variableType in fetchRequest.substitutionVariables %}
, {{ variableName | lowerFirstWord }}: {{ variableType }}
{% endfor %}
{% endfilter %}) throws -> [{{ resultTypeName }}] {
guard let persistentStoreCoordinator = managedObjectContext.persistentStoreCoordinator else {
fatalError("Managed object context has no persistent store coordinator for getting fetch request templates")
}
let model = persistentStoreCoordinator.managedObjectModel
let substitutionVariables: [String: Any] = [
{% for variableName, variableType in fetchRequest.substitutionVariables %}
"{{ variableName }}": {{ variableName | lowerFirstWord }}{{ "," if not forloop.last }}
{% empty %}
:
{% endfor %}
]
guard let fetchRequest = model.fetchRequestFromTemplate(withName: "{{ fetchRequest.name }}", substitutionVariables: substitutionVariables) else {
fatalError("No fetch request template named '{{ fetchRequest.name }}' found.")
}
guard let result = try managedObjectContext.fetch(fetchRequest) as? [{{ resultTypeName }}] else {
fatalError("Unable to cast fetch result to correct result type.")
}
return result
}
{% endfor %}
}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
// swiftlint:enable identifier_name line_length type_body_length

View file

@ -0,0 +1,211 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
// swiftlint:disable superfluous_disable_command implicit_return
// swiftlint:disable sorted_imports
import CoreData
import Foundation
{% for import in param.extraImports %}
import {{ import }}
{% empty %}
{# If extraImports is a single String instead of an array, `for` considers it empty but we still have to check if there's a single String value #}
{% if param.extraImports %}import {{ param.extraImports }}{% endif %}
{% endfor %}
// swiftlint:disable attributes file_length vertical_whitespace_closing_braces
// swiftlint:disable identifier_name line_length type_body_length
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% for model in models %}
{% for name, entity in model.entities %}
{% set superclass %}{{ model.entities[entity.superEntity].className|default:"NSManagedObject" }}{% endset %}
{% set entityClassName %}{{ entity.className|default:"NSManagedObject" }}{% endset %}
// MARK: - {{ entity.name }}
{% if not entity.shouldGenerateCode %}
// Note: '{{ entity.name }}' has codegen enabled for Xcode, skipping code generation.
{% elif entityClassName|contains:"." %}
// Warning: '{{ entityClassName }}' cannot be a valid type name, skipping code generation.
{% else %}
{% if param.generateObjcName %}
@objc({{ entityClassName }})
{% endif %}
{{ accessModifier }} class {{ entityClassName }}: {{ superclass }} {
{% set override %}{% if superclass != "NSManagedObject" %}override {% endif %}{% endset %}
{{ override }}{{ accessModifier }} class var entityName: String {
return "{{ entity.name }}"
}
{{ override }}{{ accessModifier }} class func entity(in managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
return NSEntityDescription.entity(forEntityName: entityName, in: managedObjectContext)
}
@available(*, deprecated, renamed: "makeFetchRequest", message: "To avoid collisions with the less concrete method in `NSManagedObject`, please use `makeFetchRequest()` instead.")
@nonobjc {{ accessModifier }} class func fetchRequest() -> NSFetchRequest<{{ entityClassName }}> {
return NSFetchRequest<{{ entityClassName }}>(entityName: entityName)
}
@nonobjc {{ accessModifier }} class func makeFetchRequest() -> NSFetchRequest<{{ entityClassName }}> {
return NSFetchRequest<{{ entityClassName }}>(entityName: entityName)
}
// swiftlint:disable discouraged_optional_boolean discouraged_optional_collection
{% for attribute in entity.attributes %}
{% if attribute.userInfo.RawType %}
{% set rawType attribute.userInfo.RawType %}
{% set unwrapOptional attribute.userInfo.unwrapOptional %}
{{ accessModifier }} var {{ attribute.name }}: {{ rawType }}{% if not unwrapOptional %}?{% endif %} {
get {
let key = "{{ attribute.name }}"
willAccessValue(forKey: key)
defer { didAccessValue(forKey: key) }
{% if unwrapOptional %}
guard let value = primitiveValue(forKey: key) as? {{ rawType }}.RawValue,
let result = {{ rawType }}(rawValue: value) else {
fatalError("Could not convert value for key '\(key)' to type '{{ rawType }}'")
}
return result
{% else %}
guard let value = primitiveValue(forKey: key) as? {{ rawType }}.RawValue else {
return nil
}
return {{ rawType }}(rawValue: value)
{% endif %}
}
set {
let key = "{{ attribute.name }}"
willChangeValue(forKey: key)
defer { didChangeValue(forKey: key) }
setPrimitiveValue(newValue{% if not unwrapOptional %}?{% endif %}.rawValue, forKey: key)
}
}
{% elif attribute.usesScalarValueType and attribute.isOptional %}
{{ accessModifier }} var {{ attribute.name }}: {{ attribute.typeName }}? {
get {
let key = "{{ attribute.name }}"
willAccessValue(forKey: key)
defer { didAccessValue(forKey: key) }
return primitiveValue(forKey: key) as? {{ attribute.typeName }}
}
set {
let key = "{{ attribute.name }}"
willChangeValue(forKey: key)
defer { didChangeValue(forKey: key) }
setPrimitiveValue(newValue, forKey: key)
}
}
{% else %}
@NSManaged {{ accessModifier }} var {{ attribute.name }}: {{ attribute.typeName }}{% if attribute.isOptional %}?{% endif %}
{% endif %}
{% endfor %}
{% for relationship in entity.relationships %}
{% if relationship.isToMany %}
@NSManaged {{ accessModifier }} var {{ relationship.name }}: {% if relationship.isOrdered %}NSOrderedSet{% else %}Set<{{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}>{% endif %}{% if relationship.isOptional %}?{% endif %}
{% else %}
@NSManaged {{ accessModifier }} var {{ relationship.name }}: {{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}{% if relationship.isOptional %}?{% endif %}
{% endif %}
{% endfor %}
{% for fetchedProperty in entity.fetchedProperties %}
@NSManaged {{ accessModifier }} var {{ fetchedProperty.name }}: [{{ fetchedProperty.fetchRequest.entity }}]
{% endfor %}
// swiftlint:enable discouraged_optional_boolean discouraged_optional_collection
}
{% for relationship in entity.relationships where relationship.isToMany %}
{% set destinationEntityClassName %}{{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}{% endset %}
{% set collectionClassName %}{% if relationship.isOrdered %}NSOrderedSet{% else %}Set<{{ destinationEntityClassName }}>{% endif %}{% endset %}
{% set relationshipName %}{{ relationship.name | upperFirstLetter }}{% endset %}
// MARK: Relationship {{ relationshipName }}
extension {{ entityClassName }} {
{% if relationship.isOrdered %}
@objc(insertObject:in{{ relationshipName }}AtIndex:)
@NSManaged public func insertInto{{ relationshipName }}(_ value: {{ destinationEntityClassName }}, at idx: Int)
@objc(removeObjectFrom{{ relationshipName }}AtIndex:)
@NSManaged public func removeFrom{{ relationshipName }}(at idx: Int)
@objc(insert{{ relationshipName }}:atIndexes:)
@NSManaged public func insertInto{{ relationshipName }}(_ values: [{{ destinationEntityClassName }}], at indexes: NSIndexSet)
@objc(remove{{ relationshipName }}AtIndexes:)
@NSManaged public func removeFrom{{ relationshipName }}(at indexes: NSIndexSet)
@objc(replaceObjectIn{{ relationshipName }}AtIndex:withObject:)
@NSManaged public func replace{{ relationshipName }}(at idx: Int, with value: {{ destinationEntityClassName }})
@objc(replace{{ relationshipName }}AtIndexes:with{{ relationshipName }}:)
@NSManaged public func replace{{ relationshipName }}(at indexes: NSIndexSet, with values: [{{ destinationEntityClassName }}])
{% endif %}
@objc(add{{ relationshipName }}Object:)
@NSManaged public func addTo{{ relationshipName }}(_ value: {{ destinationEntityClassName }})
@objc(remove{{ relationshipName }}Object:)
@NSManaged public func removeFrom{{ relationshipName }}(_ value: {{ destinationEntityClassName }})
@objc(add{{ relationshipName }}:)
@NSManaged public func addTo{{ relationshipName }}(_ values: {{ collectionClassName }})
@objc(remove{{ relationshipName }}:)
@NSManaged public func removeFrom{{ relationshipName }}(_ values: {{ collectionClassName }})
}
{% endfor %}
{% if model.fetchRequests[entity.name].count > 0 %}
// MARK: Fetch Requests
extension {{ entityClassName }} {
{% for fetchRequest in model.fetchRequests[entity.name] %}
{% set resultTypeName %}{% filter removeNewlines:"leading" %}
{% if fetchRequest.resultType == "Object" %}
{{ entityClassName }}
{% elif fetchRequest.resultType == "Object ID" %}
NSManagedObjectID
{% elif fetchRequest.resultType == "Dictionary" %}
[String: Any]
{% endif %}
{% endfilter %}{% endset %}
class func fetch{{ fetchRequest.name | upperFirstLetter }}({% filter removeNewlines:"leading" %}
managedObjectContext: NSManagedObjectContext
{% for variableName, variableType in fetchRequest.substitutionVariables %}
, {{ variableName | lowerFirstWord }}: {{ variableType }}
{% endfor %}
{% endfilter %}) throws -> [{{ resultTypeName }}] {
guard let persistentStoreCoordinator = managedObjectContext.persistentStoreCoordinator else {
fatalError("Managed object context has no persistent store coordinator for getting fetch request templates")
}
let model = persistentStoreCoordinator.managedObjectModel
let substitutionVariables: [String: Any] = [
{% for variableName, variableType in fetchRequest.substitutionVariables %}
"{{ variableName }}": {{ variableName | lowerFirstWord }}{{ "," if not forloop.last }}
{% empty %}
:
{% endfor %}
]
guard let fetchRequest = model.fetchRequestFromTemplate(withName: "{{ fetchRequest.name }}", substitutionVariables: substitutionVariables) else {
fatalError("No fetch request template named '{{ fetchRequest.name }}' found.")
}
guard let result = try managedObjectContext.fetch(fetchRequest) as? [{{ resultTypeName }}] else {
fatalError("Unable to cast fetch result to correct result type.")
}
return result
}
{% endfor %}
}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
// swiftlint:enable identifier_name line_length type_body_length

View file

@ -0,0 +1,103 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if groups.count > 0 %}
{% set enumName %}{{param.enumName|default:"Files"}}{% endset %}
{% set useExt %}{% if param.useExtension|default:"true" %}true{% endif %}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set resourceType %}{{param.resourceTypeName|default:"File"}}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length line_length implicit_return
// MARK: - Files
{% macro groupBlock group %}
{% for file in group.files %}
{% call fileBlock file %}
{% endfor %}
{% for dir in group.directories %}
{% call dirBlock dir %}
{% endfor %}
{% endmacro %}
{% macro fileBlock file %}
/// {% if file.path and param.preservePath %}{{file.path}}/{% endif %}{{file.name}}{% if file.ext %}.{{file.ext}}{% endif %}
{% set identifier %}{{ file.name }}{% if useExt %}.{{ file.ext }}{% endif %}{% endset %}
{{accessModifier}} static let {{identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{resourceType}}(name: "{{file.name}}", ext: {% if file.ext %}"{{file.ext}}"{% else %}nil{% endif %}, relativePath: "{{file.path if param.preservePath}}", mimeType: "{{file.mimeType}}")
{% endmacro %}
{% macro dirBlock directory %}
{% for file in directory.files %}
{% call fileBlock file %}
{% endfor %}
{% for dir in directory.directories %}
{% call dirBlock dir %}
{% endfor %}
{% endmacro %}
// swiftlint:disable explicit_type_interface identifier_name
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
{{accessModifier}} enum {{enumName}} {
{% if groups.count > 1 or param.forceFileNameEnum %}
{% for group in groups %}
{{accessModifier}} enum {{group.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call groupBlock group %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call groupBlock groups.first %}
{% endif %}
}
// swiftlint:enable explicit_type_interface identifier_name
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
{{accessModifier}} struct {{resourceType}} {
{{accessModifier}} let name: String
{{accessModifier}} let ext: String?
{{accessModifier}} let relativePath: String
{{accessModifier}} let mimeType: String
{{accessModifier}} var url: URL {
return url(locale: nil)
}
{{accessModifier}} func url(locale: Locale?) -> URL {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
let url = bundle.url(
forResource: name,
withExtension: ext,
subdirectory: relativePath,
localization: locale?.identifier
)
guard let result = url else {
let file = name + (ext.flatMap { ".\($0)" } ?? "")
fatalError("Could not locate file named \(file)")
}
return result
}
{{accessModifier}} var path: String {
return path(locale: nil)
}
{{accessModifier}} func path(locale: Locale?) -> String {
return url(locale: locale).path
}
}
{% if not param.bundle %}
// swiftlint:disable convenience_type explicit_type_interface
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type explicit_type_interface
{% endif %}
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,103 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if groups.count > 0 %}
{% set enumName %}{{param.enumName|default:"Files"}}{% endset %}
{% set useExt %}{% if param.useExtension|default:"true" %}true{% endif %}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set resourceType %}{{param.resourceTypeName|default:"File"}}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length line_length implicit_return
// MARK: - Files
{% macro groupBlock group %}
{% for file in group.files %}
{% call fileBlock file %}
{% endfor %}
{% for dir in group.directories %}
{% call dirBlock dir %}
{% endfor %}
{% endmacro %}
{% macro fileBlock file %}
/// {% if file.path and param.preservePath %}{{file.path}}/{% endif %}{{file.name}}{% if file.ext %}.{{file.ext}}{% endif %}
{% set identifier %}{{ file.name }}{% if useExt %}.{{ file.ext }}{% endif %}{% endset %}
{{accessModifier}} static let {{identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{resourceType}}(name: "{{file.name}}", ext: {% if file.ext %}"{{file.ext}}"{% else %}nil{% endif %}, relativePath: "{{file.path if param.preservePath}}", mimeType: "{{file.mimeType}}")
{% endmacro %}
{% macro dirBlock directory %}
{% for file in directory.files %}
{% call fileBlock file %}
{% endfor %}
{% for dir in directory.directories %}
{% call dirBlock dir %}
{% endfor %}
{% endmacro %}
// swiftlint:disable explicit_type_interface identifier_name
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
{{accessModifier}} enum {{enumName}} {
{% if groups.count > 1 or param.forceFileNameEnum %}
{% for group in groups %}
{{accessModifier}} enum {{group.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call groupBlock group %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call groupBlock groups.first %}
{% endif %}
}
// swiftlint:enable explicit_type_interface identifier_name
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
{{accessModifier}} struct {{resourceType}} {
{{accessModifier}} let name: String
{{accessModifier}} let ext: String?
{{accessModifier}} let relativePath: String
{{accessModifier}} let mimeType: String
{{accessModifier}} var url: URL {
return url(locale: nil)
}
{{accessModifier}} func url(locale: Locale?) -> URL {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
let url = bundle.url(
forResource: name,
withExtension: ext,
subdirectory: relativePath,
localization: locale?.identifier
)
guard let result = url else {
let file = name + (ext.flatMap { ".\($0)" } ?? "")
fatalError("Could not locate file named \(file)")
}
return result
}
{{accessModifier}} var path: String {
return path(locale: nil)
}
{{accessModifier}} func path(locale: Locale?) -> String {
return url(locale: locale).path
}
}
{% if not param.bundle %}
// swiftlint:disable convenience_type explicit_type_interface
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type explicit_type_interface
{% endif %}
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,107 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if groups.count > 0 %}
{% set enumName %}{{param.enumName|default:"Files"}}{% endset %}
{% set useExt %}{% if param.useExtension|default:"true" %}true{% endif %}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set resourceType %}{{param.resourceTypeName|default:"File"}}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length line_length implicit_return
// MARK: - Files
{% macro groupBlock group %}
{% for file in group.files %}
{% call fileBlock file %}
{% endfor %}
{% for dir in group.directories %}
{% call dirBlock dir "" %}
{% endfor %}
{% endmacro %}
{% macro fileBlock file %}
/// {% if file.path and param.preservePath %}{{file.path}}/{% endif %}{{file.name}}{% if file.ext %}.{{file.ext}}{% endif %}
{% set identifier %}{{ file.name }}{% if useExt %}.{{ file.ext }}{% endif %}{% endset %}
{{accessModifier}} static let {{identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{resourceType}}(name: "{{file.name}}", ext: {% if file.ext %}"{{file.ext}}"{% else %}nil{% endif %}, relativePath: "{{file.path if param.preservePath}}", mimeType: "{{file.mimeType}}")
{% endmacro %}
{% macro dirBlock directory parent %}
{% set fullDir %}{{parent}}{{directory.name}}/{% endset %}
/// {{ fullDir }}
{{accessModifier}} enum {{directory.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% for file in directory.files %}
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
{% endfor %}
{% for dir in directory.directories %}
{% filter indent:2 %}{% call dirBlock dir fullDir %}{% endfilter %}
{% endfor %}
}
{% endmacro %}
// swiftlint:disable explicit_type_interface identifier_name
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
{{accessModifier}} enum {{enumName}} {
{% if groups.count > 1 or param.forceFileNameEnum %}
{% for group in groups %}
{{accessModifier}} enum {{group.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call groupBlock group %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call groupBlock groups.first %}
{% endif %}
}
// swiftlint:enable explicit_type_interface identifier_name
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
{{accessModifier}} struct {{resourceType}} {
{{accessModifier}} let name: String
{{accessModifier}} let ext: String?
{{accessModifier}} let relativePath: String
{{accessModifier}} let mimeType: String
{{accessModifier}} var url: URL {
return url(locale: nil)
}
{{accessModifier}} func url(locale: Locale?) -> URL {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
let url = bundle.url(
forResource: name,
withExtension: ext,
subdirectory: relativePath,
localization: locale?.identifier
)
guard let result = url else {
let file = name + (ext.flatMap { ".\($0)" } ?? "")
fatalError("Could not locate file named \(file)")
}
return result
}
{{accessModifier}} var path: String {
return path(locale: nil)
}
{{accessModifier}} func path(locale: Locale?) -> String {
return url(locale: locale).path
}
}
{% if not param.bundle %}
// swiftlint:disable convenience_type explicit_type_interface
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type explicit_type_interface
{% endif %}
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,107 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if groups.count > 0 %}
{% set enumName %}{{param.enumName|default:"Files"}}{% endset %}
{% set useExt %}{% if param.useExtension|default:"true" %}true{% endif %}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set resourceType %}{{param.resourceTypeName|default:"File"}}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length line_length implicit_return
// MARK: - Files
{% macro groupBlock group %}
{% for file in group.files %}
{% call fileBlock file %}
{% endfor %}
{% for dir in group.directories %}
{% call dirBlock dir "" %}
{% endfor %}
{% endmacro %}
{% macro fileBlock file %}
/// {% if file.path and param.preservePath %}{{file.path}}/{% endif %}{{file.name}}{% if file.ext %}.{{file.ext}}{% endif %}
{% set identifier %}{{ file.name }}{% if useExt %}.{{ file.ext }}{% endif %}{% endset %}
{{accessModifier}} static let {{identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{resourceType}}(name: "{{file.name}}", ext: {% if file.ext %}"{{file.ext}}"{% else %}nil{% endif %}, relativePath: "{{file.path if param.preservePath}}", mimeType: "{{file.mimeType}}")
{% endmacro %}
{% macro dirBlock directory parent %}
{% set fullDir %}{{parent}}{{directory.name}}/{% endset %}
/// {{ fullDir }}
{{accessModifier}} enum {{directory.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% for file in directory.files %}
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
{% endfor %}
{% for dir in directory.directories %}
{% filter indent:2 %}{% call dirBlock dir fullDir %}{% endfilter %}
{% endfor %}
}
{% endmacro %}
// swiftlint:disable explicit_type_interface identifier_name
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
{{accessModifier}} enum {{enumName}} {
{% if groups.count > 1 or param.forceFileNameEnum %}
{% for group in groups %}
{{accessModifier}} enum {{group.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call groupBlock group %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call groupBlock groups.first %}
{% endif %}
}
// swiftlint:enable explicit_type_interface identifier_name
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
{{accessModifier}} struct {{resourceType}} {
{{accessModifier}} let name: String
{{accessModifier}} let ext: String?
{{accessModifier}} let relativePath: String
{{accessModifier}} let mimeType: String
{{accessModifier}} var url: URL {
return url(locale: nil)
}
{{accessModifier}} func url(locale: Locale?) -> URL {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
let url = bundle.url(
forResource: name,
withExtension: ext,
subdirectory: relativePath,
localization: locale?.identifier
)
guard let result = url else {
let file = name + (ext.flatMap { ".\($0)" } ?? "")
fatalError("Could not locate file named \(file)")
}
return result
}
{{accessModifier}} var path: String {
return path(locale: nil)
}
{{accessModifier}} func path(locale: Locale?) -> String {
return url(locale: locale).path
}
}
{% if not param.bundle %}
// swiftlint:disable convenience_type explicit_type_interface
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type explicit_type_interface
{% endif %}
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,110 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if families %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set fontType %}{{param.fontTypeName|default:"FontConvertible"}}{% endset %}
#if os(macOS)
import AppKit.NSFont
#elseif os(iOS) || os(tvOS) || os(watchOS)
import UIKit.UIFont
#endif
// Deprecated typealiases
@available(*, deprecated, renamed: "{{fontType}}.Font", message: "This typealias will be removed in SwiftGen 7.0")
{{accessModifier}} typealias {{param.fontAliasName|default:"Font"}} = {{fontType}}.Font
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// swiftlint:disable implicit_return
// MARK: - Fonts
// swiftlint:disable identifier_name line_length type_body_length
{% macro transformPath path %}{% filter removeNewlines %}
{% if param.preservePath %}
{{path}}
{% else %}
{{path|basename}}
{% endif %}
{% endfilter %}{% endmacro %}
{{accessModifier}} enum {{param.enumName|default:"FontFamily"}} {
{% for family in families %}
{{accessModifier}} enum {{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% for font in family.fonts %}
{{accessModifier}} static let {{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{fontType}}(name: "{{font.name}}", family: "{{family.name}}", path: "{% call transformPath font.path %}")
{% endfor %}
{{accessModifier}} static let all: [{{fontType}}] = [{% for font in family.fonts %}{{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{{ ", " if not forloop.last }}{% endfor %}]
}
{% endfor %}
{{accessModifier}} static let allCustomFonts: [{{fontType}}] = [{% for family in families %}{{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.all{{ ", " if not forloop.last }}{% endfor %}].flatMap { $0 }
{{accessModifier}} static func registerAllCustomFonts() {
allCustomFonts.forEach { $0.register() }
}
}
// swiftlint:enable identifier_name line_length type_body_length
// MARK: - Implementation Details
{{accessModifier}} struct {{fontType}} {
{{accessModifier}} let name: String
{{accessModifier}} let family: String
{{accessModifier}} let path: String
#if os(macOS)
{{accessModifier}} typealias Font = NSFont
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Font = UIFont
#endif
{{accessModifier}} func font(size: CGFloat) -> Font! {
return Font(font: self, size: size)
}
{{accessModifier}} func register() {
// swiftlint:disable:next conditional_returns_on_newline
guard let url = url else { return }
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
}
fileprivate var url: URL? {
{% if param.lookupFunction %}
return {{param.lookupFunction}}(name, family, path)
{% else %}
return {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil)
{% endif %}
}
}
{{accessModifier}} extension {{fontType}}.Font {
convenience init?(font: {{fontType}}, size: CGFloat) {
#if os(iOS) || os(tvOS) || os(watchOS)
if !UIFont.fontNames(forFamilyName: font.family).contains(font.name) {
font.register()
}
#elseif os(macOS)
if let url = font.url, CTFontManagerGetScopeForURL(url as CFURL) == .none {
font.register()
}
#endif
self.init(name: font.name, size: size)
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No fonts found
{% endif %}

View file

@ -0,0 +1,113 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if families %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set fontType %}{{param.fontTypeName|default:"FontConvertible"}}{% endset %}
#if os(macOS)
import AppKit.NSFont
#elseif os(iOS) || os(tvOS) || os(watchOS)
import UIKit.UIFont
#endif
// Deprecated typealiases
@available(*, deprecated, renamed: "{{fontType}}.Font", message: "This typealias will be removed in SwiftGen 7.0")
{{accessModifier}} typealias {{param.fontAliasName|default:"Font"}} = {{fontType}}.Font
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Fonts
// swiftlint:disable identifier_name line_length type_body_length
{% macro transformPath path %}{% filter removeNewlines %}
{% if param.preservePath %}
{{path}}
{% else %}
{{path|basename}}
{% endif %}
{% endfilter %}{% endmacro %}
{{accessModifier}} enum {{param.enumName|default:"FontFamily"}} {
{% for family in families %}
{{accessModifier}} enum {{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% for font in family.fonts %}
{{accessModifier}} static let {{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{fontType}}(name: "{{font.name}}", family: "{{family.name}}", path: "{% call transformPath font.path %}")
{% endfor %}
{{accessModifier}} static let all: [{{fontType}}] = [{% for font in family.fonts %}{{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{{ ", " if not forloop.last }}{% endfor %}]
}
{% endfor %}
{{accessModifier}} static let allCustomFonts: [{{fontType}}] = [{% for family in families %}{{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.all{{ ", " if not forloop.last }}{% endfor %}].flatMap { $0 }
{{accessModifier}} static func registerAllCustomFonts() {
allCustomFonts.forEach { $0.register() }
}
}
// swiftlint:enable identifier_name line_length type_body_length
// MARK: - Implementation Details
{{accessModifier}} struct {{fontType}} {
{{accessModifier}} let name: String
{{accessModifier}} let family: String
{{accessModifier}} let path: String
#if os(macOS)
{{accessModifier}} typealias Font = NSFont
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Font = UIFont
#endif
{{accessModifier}} func font(size: CGFloat) -> Font {
guard let font = Font(font: self, size: size) else {
fatalError("Unable to initialize font '\(name)' (\(family))")
}
return font
}
{{accessModifier}} func register() {
// swiftlint:disable:next conditional_returns_on_newline
guard let url = url else { return }
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
}
fileprivate var url: URL? {
// swiftlint:disable:next implicit_return
{% if param.lookupFunction %}
return {{param.lookupFunction}}(name, family, path)
{% else %}
return {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil)
{% endif %}
}
}
{{accessModifier}} extension {{fontType}}.Font {
convenience init?(font: {{fontType}}, size: CGFloat) {
#if os(iOS) || os(tvOS) || os(watchOS)
if !UIFont.fontNames(forFamilyName: font.family).contains(font.name) {
font.register()
}
#elseif os(macOS)
if let url = font.url, CTFontManagerGetScopeForURL(url as CFURL) == .none {
font.register()
}
#endif
self.init(name: font.name, size: size)
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No fonts found
{% endif %}

View file

@ -0,0 +1,157 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if platform and storyboards %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set isAppKit %}{% if platform == "macOS" %}true{% endif %}{% endset %}
{% set prefix %}{% if isAppKit %}NS{% else %}UI{% endif %}{% endset %}
{% set controller %}{% if isAppKit %}Controller{% else %}ViewController{% endif %}{% endset %}
// swiftlint:disable sorted_imports
import Foundation
{% for module in modules where module != env.PRODUCT_MODULE_NAME and module != param.module %}
import {{module}}
{% endfor %}
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length implicit_return
// MARK: - Storyboard Scenes
// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
{% macro moduleName item %}{% filter removeNewlines %}
{% if item.moduleIsPlaceholder %}
{{ env.PRODUCT_MODULE_NAME|default:param.module }}
{% else %}
{{ item.module }}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro className item %}{% filter removeNewlines %}
{% set module %}{% call moduleName item %}{% endset %}
{% if module and ( not param.ignoreTargetModule or module != env.PRODUCT_MODULE_NAME and module != param.module ) %}
{{module}}.
{% endif %}
{{item.type}}
{% endfilter %}{% endmacro %}
{{accessModifier}} enum {{param.enumName|default:"StoryboardScene"}} {
{% for storyboard in storyboards %}
{% set storyboardName %}{{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}{% endset %}
{{accessModifier}} enum {{storyboardName}}: StoryboardType {
{{accessModifier}} static let storyboardName = "{{storyboard.name}}"
{% if storyboard.initialScene %}
{% set sceneClass %}{% call className storyboard.initialScene %}{% endset %}
{{accessModifier}} static let initialScene = InitialSceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self)
{% endif %}
{% for scene in storyboard.scenes %}
{% set sceneID %}{{scene.identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set sceneClass %}{% call className scene %}{% endset %}
{{accessModifier}} static let {{sceneID}} = SceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self, identifier: "{{scene.identifier}}")
{% endfor %}
}
{% endfor %}
}
// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
// MARK: - Implementation Details
{{accessModifier}} protocol StoryboardType {
static var storyboardName: String { get }
}
{{accessModifier}} extension StoryboardType {
static var storyboard: {{prefix}}Storyboard {
let name = {% if isAppKit %}NSStoryboard.Name({% endif %}self.storyboardName{% if isAppKit %}){% endif %}
{% if param.lookupFunction %}
return {{param.lookupFunction}}(name)
{% else %}
return {{prefix}}Storyboard(name: name, bundle: {{param.bundle|default:"BundleToken.bundle"}})
{% endif %}
}
}
{{accessModifier}} struct SceneType<T{% if not isAppKit %}: UIViewController{% endif %}> {
{{accessModifier}} let storyboard: StoryboardType.Type
{{accessModifier}} let identifier: String
{{accessModifier}} func instantiate() -> T {
let identifier = {% if isAppKit %}NSStoryboard.SceneIdentifier({% endif %}self.identifier{% if isAppKit %}){% endif %}
guard let controller = storyboard.storyboard.instantiate{{controller}}(withIdentifier: identifier) as? T else {
fatalError("{{controller}} '\(identifier)' is not of the expected class \(T.self).")
}
return controller
}
{% if isAppKit %}
@available(macOS 10.15, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSViewController {
return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
}
@available(macOS 10.15, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSWindowController {
return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
}
{% else %}
@available(iOS 13.0, tvOS 13.0, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T {
return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
}
{% endif %}
}
{{accessModifier}} struct InitialSceneType<T{% if not isAppKit %}: UIViewController{% endif %}> {
{{accessModifier}} let storyboard: StoryboardType.Type
{{accessModifier}} func instantiate() -> T {
guard let controller = storyboard.storyboard.instantiateInitial{{controller}}() as? T else {
fatalError("{{controller}} is not of the expected class \(T.self).")
}
return controller
}
{% if isAppKit %}
@available(macOS 10.15, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSViewController {
guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
}
return controller
}
@available(macOS 10.15, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSWindowController {
guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
}
return controller
}
{% else %}
@available(iOS 13.0, tvOS 13.0, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T {
guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
}
return controller
}
{% endif %}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% elif storyboards %}
// Mixed AppKit and UIKit storyboard files found, please invoke swiftgen with these separately
{% else %}
// No storyboard found
{% endif %}

View file

@ -0,0 +1,159 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if platform and storyboards %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set isAppKit %}{% if platform == "macOS" %}true{% endif %}{% endset %}
{% set prefix %}{% if isAppKit %}NS{% else %}UI{% endif %}{% endset %}
{% set controller %}{% if isAppKit %}Controller{% else %}ViewController{% endif %}{% endset %}
// swiftlint:disable sorted_imports
import Foundation
{% for module in modules where module != env.PRODUCT_MODULE_NAME and module != param.module %}
import {{module}}
{% endfor %}
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length implicit_return
// MARK: - Storyboard Scenes
// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
{% macro moduleName item %}{% filter removeNewlines %}
{% if item.moduleIsPlaceholder %}
{{ env.PRODUCT_MODULE_NAME|default:param.module }}
{% else %}
{{ item.module }}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro className item %}{% filter removeNewlines %}
{% set module %}{% call moduleName item %}{% endset %}
{% if module and ( not param.ignoreTargetModule or module != env.PRODUCT_MODULE_NAME and module != param.module ) %}
{{module}}.
{% endif %}
{{item.type}}
{% endfilter %}{% endmacro %}
{{accessModifier}} enum {{param.enumName|default:"StoryboardScene"}} {
{% for storyboard in storyboards %}
{% set storyboardName %}{{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}{% endset %}
{{accessModifier}} enum {{storyboardName}}: StoryboardType {
{{accessModifier}} static let storyboardName = "{{storyboard.name}}"
{% if storyboard.initialScene %}
{% set sceneClass %}{% call className storyboard.initialScene %}{% endset %}
{{accessModifier}} static let initialScene = InitialSceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self)
{% endif %}
{% for scene in storyboard.scenes %}
{% set sceneID %}{{scene.identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set sceneClass %}{% call className scene %}{% endset %}
{{accessModifier}} static let {{sceneID}} = SceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self, identifier: "{{scene.identifier}}")
{% endfor %}
}
{% endfor %}
}
// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
// MARK: - Implementation Details
{{accessModifier}} protocol StoryboardType {
static var storyboardName: String { get }
}
{{accessModifier}} extension StoryboardType {
static var storyboard: {{prefix}}Storyboard {
let name = {% if isAppKit %}NSStoryboard.Name({% endif %}self.storyboardName{% if isAppKit %}){% endif %}
{% if param.lookupFunction %}
return {{param.lookupFunction}}(name)
{% else %}
return {{prefix}}Storyboard(name: name, bundle: {{param.bundle|default:"BundleToken.bundle"}})
{% endif %}
}
}
{{accessModifier}} struct SceneType<T{% if not isAppKit %}: UIViewController{% endif %}> {
{{accessModifier}} let storyboard: StoryboardType.Type
{{accessModifier}} let identifier: String
{{accessModifier}} func instantiate() -> T {
let identifier = {% if isAppKit %}NSStoryboard.SceneIdentifier({% endif %}self.identifier{% if isAppKit %}){% endif %}
guard let controller = storyboard.storyboard.instantiate{{controller}}(withIdentifier: identifier) as? T else {
fatalError("{{controller}} '\(identifier)' is not of the expected class \(T.self).")
}
return controller
}
{% if isAppKit %}
@available(macOS 10.15, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSViewController {
let identifier = NSStoryboard.SceneIdentifier(self.identifier)
return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
}
@available(macOS 10.15, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSWindowController {
let identifier = NSStoryboard.SceneIdentifier(self.identifier)
return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
}
{% else %}
@available(iOS 13.0, tvOS 13.0, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T {
return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
}
{% endif %}
}
{{accessModifier}} struct InitialSceneType<T{% if not isAppKit %}: UIViewController{% endif %}> {
{{accessModifier}} let storyboard: StoryboardType.Type
{{accessModifier}} func instantiate() -> T {
guard let controller = storyboard.storyboard.instantiateInitial{{controller}}() as? T else {
fatalError("{{controller}} is not of the expected class \(T.self).")
}
return controller
}
{% if isAppKit %}
@available(macOS 10.15, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSViewController {
guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
}
return controller
}
@available(macOS 10.15, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSWindowController {
guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
}
return controller
}
{% else %}
@available(iOS 13.0, tvOS 13.0, *)
{{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T {
guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
}
return controller
}
{% endif %}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% elif storyboards %}
// Mixed AppKit and UIKit storyboard files found, please invoke swiftgen with these separately
{% else %}
// No storyboard found
{% endif %}

View file

@ -0,0 +1,60 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if platform and storyboards %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set isAppKit %}{% if platform == "macOS" %}true{% endif %}{% endset %}
// swiftlint:disable sorted_imports
import Foundation
{% for module in modules where module != env.PRODUCT_MODULE_NAME and module != param.module %}
import {{module}}
{% endfor %}
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Storyboard Segues
// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
{{accessModifier}} enum {{param.enumName|default:"StoryboardSegue"}} {
{% for storyboard in storyboards where storyboard.segues %}
{{accessModifier}} enum {{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: String, SegueType {
{% for segue in storyboard.segues %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
case {{segueID|escapeReservedKeywords}}{% if segueID != segue.identifier %} = "{{segue.identifier}}"{% endif %}
{% endfor %}
}
{% endfor %}
}
// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
// MARK: - Implementation Details
{{accessModifier}} protocol SegueType: RawRepresentable {}
{{accessModifier}} extension {% if isAppKit %}NSSeguePerforming{% else %}UIViewController{% endif %} {
func perform<S: SegueType>(segue: S, sender: Any? = nil) where S.RawValue == String {
let identifier = {% if isAppKit %}NSStoryboardSegue.Identifier({% endif %}segue.rawValue{% if isAppKit %}){% endif %}
performSegue{% if isAppKit %}?{% endif %}(withIdentifier: identifier, sender: sender)
}
}
{{accessModifier}} extension SegueType where RawValue == String {
init?(_ segue: {% if isAppKit %}NS{% else %}UI{% endif %}StoryboardSegue) {
{% if isAppKit %}
#if swift(>=4.2)
guard let identifier = segue.identifier else { return nil }
#else
guard let identifier = segue.identifier?.rawValue else { return nil }
#endif
{% else %}
guard let identifier = segue.identifier else { return nil }
{% endif %}
self.init(rawValue: identifier)
}
}
{% elif storyboards %}
// Mixed AppKit and UIKit storyboard files found, please invoke swiftgen with these separately
{% else %}
// No storyboard found
{% endif %}

View file

@ -0,0 +1,60 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if platform and storyboards %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set isAppKit %}{% if platform == "macOS" %}true{% endif %}{% endset %}
// swiftlint:disable sorted_imports
import Foundation
{% for module in modules where module != env.PRODUCT_MODULE_NAME and module != param.module %}
import {{module}}
{% endfor %}
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Storyboard Segues
// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
{{accessModifier}} enum {{param.enumName|default:"StoryboardSegue"}} {
{% for storyboard in storyboards where storyboard.segues %}
{{accessModifier}} enum {{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: String, SegueType {
{% for segue in storyboard.segues %}
{% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
case {{segueID|escapeReservedKeywords}}{% if segueID != segue.identifier %} = "{{segue.identifier}}"{% endif %}
{% endfor %}
}
{% endfor %}
}
// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
// MARK: - Implementation Details
{{accessModifier}} protocol SegueType: RawRepresentable {}
{{accessModifier}} extension {% if isAppKit %}NSSeguePerforming{% else %}UIViewController{% endif %} {
func perform<S: SegueType>(segue: S, sender: Any? = nil) where S.RawValue == String {
let identifier = {% if isAppKit %}NSStoryboardSegue.Identifier({% endif %}segue.rawValue{% if isAppKit %}){% endif %}
performSegue{% if isAppKit %}?{% endif %}(withIdentifier: identifier, sender: sender)
}
}
{{accessModifier}} extension SegueType where RawValue == String {
init?(_ segue: {% if isAppKit %}NS{% else %}UI{% endif %}StoryboardSegue) {
{% if isAppKit %}
#if swift(>=4.2)
guard let identifier = segue.identifier else { return nil }
#else
guard let identifier = segue.identifier?.rawValue else { return nil }
#endif
{% else %}
guard let identifier = segue.identifier else { return nil }
{% endif %}
self.init(rawValue: identifier)
}
}
{% elif storyboards %}
// Mixed AppKit and UIKit storyboard files found, please invoke swiftgen with these separately
{% else %}
// No storyboard found
{% endif %}

View file

@ -0,0 +1,82 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - JSON Files
{% macro fileBlock file %}
{% call documentBlock file file.document %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% elif document.metadata.type == "Dictionary" %}
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value document.data %}
{% endfor %}
{% else %}
{{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% elif metadata.type == "Optional" %}
Any?
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
{% endfilter %}{% endmacro %}
{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "String" %}
"{{ value }}"
{% elif metadata.type == "Optional" %}
nil
{% elif metadata.type == "Array" and value %}
[{% for value in value %}
{% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
{{ ", " if not forloop.last }}
{% endfor %}]
{% elif metadata.type == "Dictionary" %}
[{% for key,value in value %}
"{{key}}": {% call valueBlock value metadata.properties[key] %}
{{ ", " if not forloop.last }}
{% empty %}
:
{% endfor %}]
{% elif metadata.type == "Bool" %}
{% if value %}true{% else %}false{% endif %}
{% else %}
{{ value }}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length number_separator type_body_length
{{accessModifier}} enum {{param.enumName|default:"JSONFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length number_separator type_body_length
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,82 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - JSON Files
{% macro fileBlock file %}
{% call documentBlock file file.document %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% elif document.metadata.type == "Dictionary" %}
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value document.data %}
{% endfor %}
{% else %}
{{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% elif metadata.type == "Optional" %}
Any?
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
{% endfilter %}{% endmacro %}
{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "String" %}
"{{ value }}"
{% elif metadata.type == "Optional" %}
nil
{% elif metadata.type == "Array" and value %}
[{% for value in value %}
{% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
{{ ", " if not forloop.last }}
{% endfor %}]
{% elif metadata.type == "Dictionary" %}
[{% for key,value in value %}
"{{key}}": {% call valueBlock value metadata.properties[key] %}
{{ ", " if not forloop.last }}
{% empty %}
:
{% endfor %}]
{% elif metadata.type == "Bool" %}
{% if value %}true{% else %}false{% endif %}
{% else %}
{{ value }}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length number_separator type_body_length
{{accessModifier}} enum {{param.enumName|default:"JSONFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length number_separator type_body_length
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,112 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - JSON Files
{% macro fileBlock file %}
{% call documentBlock file file.document %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = objectFromJSON(at: "{% call transformPath file.path %}")
{% elif document.metadata.type == "Dictionary" %}
private static let _document = JSONDocument(path: "{% call transformPath file.path %}")
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value %}
{% endfor %}
{% else %}
{{accessModifier}} static let value: {{rootType}} = objectFromJSON(at: "{% call transformPath file.path %}")
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% elif metadata.type == "Optional" %}
Any?
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = _document["{{key}}"]
{% endfilter %}{% endmacro %}
{% macro transformPath path %}{% filter removeNewlines %}
{% if param.preservePath %}
{{path}}
{% else %}
{{path|basename}}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length type_body_length
{{accessModifier}} enum {{param.enumName|default:"JSONFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length type_body_length
// MARK: - Implementation Details
private func objectFromJSON<T>(at path: String) -> T {
{% if param.lookupFunction %}
guard let url = {{param.lookupFunction}}(path),
{% else %}
guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
{% endif %}
let json = try? JSONSerialization.jsonObject(with: Data(contentsOf: url), options: []),
let result = json as? T else {
fatalError("Unable to load JSON at path: \(path)")
}
return result
}
private struct JSONDocument {
let data: [String: Any]
init(path: String) {
self.data = objectFromJSON(at: path)
}
subscript<T>(key: String) -> T {
guard let result = data[key] as? T else {
fatalError("Property '\(key)' is not of type \(T.self)")
}
return result
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,112 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - JSON Files
{% macro fileBlock file %}
{% call documentBlock file file.document %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = objectFromJSON(at: "{% call transformPath file.path %}")
{% elif document.metadata.type == "Dictionary" %}
private static let _document = JSONDocument(path: "{% call transformPath file.path %}")
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value %}
{% endfor %}
{% else %}
{{accessModifier}} static let value: {{rootType}} = objectFromJSON(at: "{% call transformPath file.path %}")
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% elif metadata.type == "Optional" %}
Any?
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = _document["{{key}}"]
{% endfilter %}{% endmacro %}
{% macro transformPath path %}{% filter removeNewlines %}
{% if param.preservePath %}
{{path}}
{% else %}
{{path|basename}}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length type_body_length
{{accessModifier}} enum {{param.enumName|default:"JSONFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length type_body_length
// MARK: - Implementation Details
private func objectFromJSON<T>(at path: String) -> T {
{% if param.lookupFunction %}
guard let url = {{param.lookupFunction}}(path),
{% else %}
guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
{% endif %}
let json = try? JSONSerialization.jsonObject(with: Data(contentsOf: url), options: []),
let result = json as? T else {
fatalError("Unable to load JSON at path: \(path)")
}
return result
}
private struct JSONDocument {
let data: [String: Any]
init(path: String) {
self.data = objectFromJSON(at: path)
}
subscript<T>(key: String) -> T {
guard let result = data[key] as? T else {
fatalError("Property '\(key)' is not of type \(T.self)")
}
return result
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,82 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Plist Files
{% macro fileBlock file %}
{% call documentBlock file file.document %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% elif document.metadata.type == "Dictionary" %}
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value document.data %}
{% endfor %}
{% else %}
{{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
{% endfilter %}{% endmacro %}
{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "String" %}
"{{ value }}"
{% elif metadata.type == "Date" %}
Date(timeIntervalSinceReferenceDate: {{ value.timeIntervalSinceReferenceDate }})
{% elif metadata.type == "Optional" %}
nil
{% elif metadata.type == "Array" and value %}
[{% for value in value %}
{% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
{{ ", " if not forloop.last }}
{% endfor %}]
{% elif metadata.type == "Dictionary" %}
[{% for key,value in value %}
"{{key}}": {% call valueBlock value metadata.properties[key] %}
{{ ", " if not forloop.last }}
{% empty %}
:
{% endfor %}]
{% elif metadata.type == "Bool" %}
{% if value %}true{% else %}false{% endif %}
{% else %}
{{ value }}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length number_separator type_body_length
{{accessModifier}} enum {{param.enumName|default:"PlistFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length number_separator type_body_length
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,82 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Plist Files
{% macro fileBlock file %}
{% call documentBlock file file.document %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% elif document.metadata.type == "Dictionary" %}
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value document.data %}
{% endfor %}
{% else %}
{{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
{% endfilter %}{% endmacro %}
{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "String" %}
"{{ value }}"
{% elif metadata.type == "Date" %}
Date(timeIntervalSinceReferenceDate: {{ value.timeIntervalSinceReferenceDate }})
{% elif metadata.type == "Optional" %}
nil
{% elif metadata.type == "Array" and value %}
[{% for value in value %}
{% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
{{ ", " if not forloop.last }}
{% endfor %}]
{% elif metadata.type == "Dictionary" %}
[{% for key,value in value %}
"{{key}}": {% call valueBlock value metadata.properties[key] %}
{{ ", " if not forloop.last }}
{% empty %}
:
{% endfor %}]
{% elif metadata.type == "Bool" %}
{% if value %}true{% else %}false{% endif %}
{% else %}
{{ value }}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length number_separator type_body_length
{{accessModifier}} enum {{param.enumName|default:"PlistFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length number_separator type_body_length
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,117 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Plist Files
{% macro fileBlock file %}
{% call documentBlock file file.document %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = arrayFromPlist(at: "{% call transformPath file.path %}")
{% elif document.metadata.type == "Dictionary" %}
private static let _document = PlistDocument(path: "{% call transformPath file.path %}")
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value %}
{% endfor %}
{% else %}
// Unsupported root type `{{rootType}}`
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = _document["{{key}}"]
{% endfilter %}{% endmacro %}
{% macro transformPath path %}{% filter removeNewlines %}
{% if param.preservePath %}
{{path}}
{% else %}
{{path|basename}}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length type_body_length
{{accessModifier}} enum {{param.enumName|default:"PlistFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length type_body_length
// MARK: - Implementation Details
private func arrayFromPlist<T>(at path: String) -> [T] {
{% if param.lookupFunction %}
guard let url = {{param.lookupFunction}}(path),
{% else %}
guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
{% endif %}
let data = NSArray(contentsOf: url) as? [T] else {
fatalError("Unable to load PLIST at path: \(path)")
}
return data
}
private struct PlistDocument {
let data: [String: Any]
init(path: String) {
{% if param.lookupFunction %}
guard let url = {{param.lookupFunction}}(path),
{% else %}
guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
{% endif %}
let data = NSDictionary(contentsOf: url) as? [String: Any] else {
fatalError("Unable to load PLIST at path: \(path)")
}
self.data = data
}
subscript<T>(key: String) -> T {
guard let result = data[key] as? T else {
fatalError("Property '\(key)' is not of type \(T.self)")
}
return result
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,117 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Plist Files
{% macro fileBlock file %}
{% call documentBlock file file.document %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = arrayFromPlist(at: "{% call transformPath file.path %}")
{% elif document.metadata.type == "Dictionary" %}
private static let _document = PlistDocument(path: "{% call transformPath file.path %}")
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value %}
{% endfor %}
{% else %}
// Unsupported root type `{{rootType}}`
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = _document["{{key}}"]
{% endfilter %}{% endmacro %}
{% macro transformPath path %}{% filter removeNewlines %}
{% if param.preservePath %}
{{path}}
{% else %}
{{path|basename}}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length type_body_length
{{accessModifier}} enum {{param.enumName|default:"PlistFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length type_body_length
// MARK: - Implementation Details
private func arrayFromPlist<T>(at path: String) -> [T] {
{% if param.lookupFunction %}
guard let url = {{param.lookupFunction}}(path),
{% else %}
guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
{% endif %}
let data = NSArray(contentsOf: url) as? [T] else {
fatalError("Unable to load PLIST at path: \(path)")
}
return data
}
private struct PlistDocument {
let data: [String: Any]
init(path: String) {
{% if param.lookupFunction %}
guard let url = {{param.lookupFunction}}(path),
{% else %}
guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
{% endif %}
let data = NSDictionary(contentsOf: url) as? [String: Any] else {
fatalError("Unable to load PLIST at path: \(path)")
}
self.data = data
}
subscript<T>(key: String) -> T {
guard let result = data[key] as? T else {
fatalError("Property '\(key)' is not of type \(T.self)")
}
return result
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,99 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if tables.count > 0 %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Strings
{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
_ p{{forloop.counter}}: Any
{% else %}
_ p{{forloop.counter}}: {{type}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
String(describing: p{{forloop.counter}})
{% elif type == "UnsafeRawPointer" %}
Int(bitPattern: p{{forloop.counter}})
{% else %}
p{{forloop.counter}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro recursiveBlock table item %}
{% for string in item.strings %}
{% if not param.noComments %}
{% for line in string.translation|split:"\n" %}
/// {{line}}
{% endfor %}
{% endif %}
{% if string.types %}
{{accessModifier}} static func {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
}
{% elif param.lookupFunction %}
{# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #}
{{accessModifier}} static var {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") }
{% else %}
{{accessModifier}} static let {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")
{% endif %}
{% endfor %}
{% for child in item.children %}
{% call recursiveBlock table child %}
{% endfor %}
{% endmacro %}
// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
{{accessModifier}} enum {{enumName}} {
{% if tables.count > 1 or param.forceFileNameEnum %}
{% for table in tables %}
{{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call recursiveBlock tables.first.name tables.first.levels %}
{% endif %}
}
// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
// MARK: - Implementation Details
extension {{enumName}} {
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
{% if param.lookupFunction %}
let format = {{ param.lookupFunction }}(key, table)
{% else %}
let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
{% endif %}
return String(format: format, locale: Locale.current, arguments: args)
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No string found
{% endif %}

View file

@ -0,0 +1,99 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if tables.count > 0 %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Strings
{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
_ p{{forloop.counter}}: Any
{% else %}
_ p{{forloop.counter}}: {{type}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
String(describing: p{{forloop.counter}})
{% elif type == "UnsafeRawPointer" %}
Int(bitPattern: p{{forloop.counter}})
{% else %}
p{{forloop.counter}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro recursiveBlock table item %}
{% for string in item.strings %}
{% if not param.noComments %}
{% for line in string.translation|split:"\n" %}
/// {{line}}
{% endfor %}
{% endif %}
{% if string.types %}
{{accessModifier}} static func {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
}
{% elif param.lookupFunction %}
{# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #}
{{accessModifier}} static var {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") }
{% else %}
{{accessModifier}} static let {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")
{% endif %}
{% endfor %}
{% for child in item.children %}
{% call recursiveBlock table child %}
{% endfor %}
{% endmacro %}
// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
{{accessModifier}} enum {{enumName}} {
{% if tables.count > 1 or param.forceFileNameEnum %}
{% for table in tables %}
{{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call recursiveBlock tables.first.name tables.first.levels %}
{% endif %}
}
// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
// MARK: - Implementation Details
extension {{enumName}} {
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
{% if param.lookupFunction %}
let format = {{ param.lookupFunction }}(key, table)
{% else %}
let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
{% endif %}
return String(format: format, locale: Locale.current, arguments: args)
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No string found
{% endif %}

View file

@ -0,0 +1,68 @@
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if tables.count > 0 %}
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
({% call paramTranslate type %})p{{ forloop.counter }}{{ " :" if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
p{{forloop.counter}}{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro paramTranslate swiftType %}
{% if swiftType == "Any" %}
id
{% elif swiftType == "CChar" %}
char
{% elif swiftType == "Float" %}
float
{% elif swiftType == "Int" %}
NSInteger
{% elif swiftType == "String" %}
id
{% elif swiftType == "UnsafePointer<CChar>" %}
char*
{% elif swiftType == "UnsafeRawPointer" %}
void*
{% else %}
objc-h.stencil is missing '{{swiftType}}'
{% endif %}
{% endmacro %}
{% macro emitOneMethod table item %}
{% for string in item.strings %}
{% if not param.noComments %}
{% for line in string.translation|split:"\n" %}
/// {{line}}
{% endfor %}
{% endif %}
{% if string.types %}
{% if string.types.count == 1 %}
+ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}}WithValue:{% call parametersBlock string.types %};
{% else %}
+ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}}WithValues:{% call parametersBlock string.types %};
{% endif %}
{% else %}
+ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}};
{% endif %}
{% endfor %}
{% for child in item.children %}
{% call emitOneMethod table child %}
{% endfor %}
{% endmacro %}
{% for table in tables %}
@interface {{ table.name }} : NSObject
{% call emitOneMethod table.name table.levels %}
@end
{% endfor %}
NS_ASSUME_NONNULL_END
{% else %}
// No strings found
{% endif %}

View file

@ -0,0 +1,90 @@
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if tables.count > 0 %}
#import "{{ param.headerName|default:"Localizable.h" }}"
{% if not param.bundle %}
@interface BundleToken : NSObject
@end
@implementation BundleToken
@end
{% endif %}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-security"
static NSString* tr(NSString *tableName, NSString *key, ...) {
NSBundle *bundle = {{param.bundle|default:"[NSBundle bundleForClass:BundleToken.class]"}};
NSString *format = [bundle localizedStringForKey:key value:nil table:tableName];
NSLocale *locale = [NSLocale currentLocale];
va_list args;
va_start(args, key);
NSString *result = [[NSString alloc] initWithFormat:format locale:locale arguments:args];
va_end(args);
return result;
};
#pragma clang diagnostic pop
{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
({% call paramTranslate type %})p{{ forloop.counter }}{{ " :" if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
p{{forloop.counter}}{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro paramTranslate swiftType %}
{% if swiftType == "Any" %}
id
{% elif swiftType == "CChar" %}
char
{% elif swiftType == "Float" %}
float
{% elif swiftType == "Int" %}
NSInteger
{% elif swiftType == "String" %}
id
{% elif swiftType == "UnsafePointer<CChar>" %}
char*
{% elif swiftType == "UnsafeRawPointer" %}
void*
{% else %}
objc-m.stencil is missing '{{swiftType}}'
{% endif %}
{% endmacro %}
{% macro tableContents table item %}
{% for string in item.strings %}
{% if string.types %}
{% if string.types.count == 1 %}
+ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}}WithValue:{% call parametersBlock string.types %}
{% else %}
+ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}}WithValues:{% call parametersBlock string.types %}
{% endif %}
{
return tr(@"{{table}}", @"{{string.key}}", {% call argumentsBlock string.types %});
}
{% else %}
+ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}} {
return tr(@"{{table}}", @"{{string.key}}");
}
{% endif %}
{% endfor %}
{% for child in item.children %}
{% call tableContents table child %}
{% endfor %}
{% endmacro %}
{% for table in tables %}
{% set tableName %}{{table.name|default:"Localized"}}{% endset %}
@implementation {{ tableName }} : NSObject
{% call tableContents table.name table.levels %}
@end
{% endfor %}
{% else %}
// No strings found
{% endif %}

View file

@ -0,0 +1,104 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if tables.count > 0 %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Strings
{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
_ p{{forloop.counter}}: Any
{% else %}
_ p{{forloop.counter}}: {{type}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
String(describing: p{{forloop.counter}})
{% elif type == "UnsafeRawPointer" %}
Int(bitPattern: p{{forloop.counter}})
{% else %}
p{{forloop.counter}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro recursiveBlock table item %}
{% for string in item.strings %}
{% if not param.noComments %}
{% for line in string.translation|split:"\n" %}
/// {{line}}
{% endfor %}
{% endif %}
{% if string.types %}
{{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
}
{% elif param.lookupFunction %}
{# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #}
{{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") }
{% else %}
{{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")
{% endif %}
{% endfor %}
{% for child in item.children %}
{{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %}
}
{% endfor %}
{% endmacro %}
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
{{accessModifier}} enum {{enumName}} {
{% if tables.count > 1 or param.forceFileNameEnum %}
{% for table in tables %}
{{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call recursiveBlock tables.first.name tables.first.levels %}
{% endif %}
}
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
extension {{enumName}} {
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
{% if param.lookupFunction %}
let format = {{ param.lookupFunction }}(key, table)
{% else %}
let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
{% endif %}
return String(format: format, locale: Locale.current, arguments: args)
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No string found
{% endif %}

View file

@ -0,0 +1,104 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if tables.count > 0 %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Strings
{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
_ p{{forloop.counter}}: Any
{% else %}
_ p{{forloop.counter}}: {{type}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
String(describing: p{{forloop.counter}})
{% elif type == "UnsafeRawPointer" %}
Int(bitPattern: p{{forloop.counter}})
{% else %}
p{{forloop.counter}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro recursiveBlock table item %}
{% for string in item.strings %}
{% if not param.noComments %}
{% for line in string.translation|split:"\n" %}
/// {{line}}
{% endfor %}
{% endif %}
{% if string.types %}
{{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
}
{% elif param.lookupFunction %}
{# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #}
{{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") }
{% else %}
{{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")
{% endif %}
{% endfor %}
{% for child in item.children %}
{{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %}
}
{% endfor %}
{% endmacro %}
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
{{accessModifier}} enum {{enumName}} {
{% if tables.count > 1 or param.forceFileNameEnum %}
{% for table in tables %}
{{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call recursiveBlock tables.first.name tables.first.levels %}
{% endif %}
}
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
extension {{enumName}} {
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
{% if param.lookupFunction %}
let format = {{ param.lookupFunction }}(key, table)
{% else %}
let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
{% endif %}
return String(format: format, locale: Locale.current, arguments: args)
}
}
{% if not param.bundle and not param.lookupFunction %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No string found
{% endif %}

View file

@ -0,0 +1,329 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if catalogs %}
{% set enumName %}{{param.enumName|default:"Asset"}}{% endset %}
{% set arResourceGroupType %}{{param.arResourceGroupTypeName|default:"ARResourceGroupAsset"}}{% endset %}
{% set colorType %}{{param.colorTypeName|default:"ColorAsset"}}{% endset %}
{% set dataType %}{{param.dataTypeName|default:"DataAsset"}}{% endset %}
{% set imageType %}{{param.imageTypeName|default:"ImageAsset"}}{% endset %}
{% set symbolType %}{{param.symbolTypeName|default:"SymbolAsset"}}{% endset %}
{% set forceNamespaces %}{{param.forceProvidesNamespaces|default:"false"}}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
#if os(macOS)
import AppKit
#elseif os(iOS)
{% if resourceCount.arresourcegroup > 0 %}
import ARKit
{% endif %}
import UIKit
#elseif os(tvOS) || os(watchOS)
import UIKit
#endif
// Deprecated typealiases
{% if resourceCount.color > 0 %}
@available(*, deprecated, renamed: "{{colorType}}.Color", message: "This typealias will be removed in SwiftGen 7.0")
{{accessModifier}} typealias {{param.colorAliasName|default:"AssetColorTypeAlias"}} = {{colorType}}.Color
{% endif %}
{% if resourceCount.image > 0 %}
@available(*, deprecated, renamed: "{{imageType}}.Image", message: "This typealias will be removed in SwiftGen 7.0")
{{accessModifier}} typealias {{param.imageAliasName|default:"AssetImageTypeAlias"}} = {{imageType}}.Image
{% endif %}
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Asset Catalogs
{% macro enumBlock assets %}
{% call casesBlock assets %}
{% if param.allValues %}
// swiftlint:disable trailing_comma
{% if resourceCount.arresourcegroup > 0 %}
{{accessModifier}} static let allResourceGroups: [{{arResourceGroupType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "arresourcegroup" "" %}{% endfilter %}
]
{% endif %}
{% if resourceCount.color > 0 %}
{{accessModifier}} static let allColors: [{{colorType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "color" "" %}{% endfilter %}
]
{% endif %}
{% if resourceCount.data > 0 %}
{{accessModifier}} static let allDataAssets: [{{dataType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "data" "" %}{% endfilter %}
]
{% endif %}
{% if resourceCount.image > 0 %}
{{accessModifier}} static let allImages: [{{imageType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "image" "" %}{% endfilter %}
]
{% endif %}
{% if resourceCount.symbol > 0 %}
{{accessModifier}} static let allSymbols: [{{symbolType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "symbol" "" %}{% endfilter %}
]
{% endif %}
// swiftlint:enable trailing_comma
{% endif %}
{% endmacro %}
{% macro casesBlock assets %}
{% for asset in assets %}
{% if asset.type == "arresourcegroup" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{arResourceGroupType}}(name: "{{asset.value}}")
{% elif asset.type == "color" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{colorType}}(name: "{{asset.value}}")
{% elif asset.type == "data" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{dataType}}(name: "{{asset.value}}")
{% elif asset.type == "image" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}")
{% elif asset.type == "symbol" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{symbolType}}(name: "{{asset.value}}")
{% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
{{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
}
{% elif asset.items %}
{% call casesBlock asset.items %}
{% endif %}
{% endfor %}
{% endmacro %}
{% macro allValuesBlock assets filter prefix %}
{% for asset in assets %}
{% if asset.type == filter %}
{{prefix}}{{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}},
{% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
{% set prefix2 %}{{prefix}}{{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.{% endset %}
{% call allValuesBlock asset.items filter prefix2 %}
{% elif asset.items %}
{% call allValuesBlock asset.items filter prefix %}
{% endif %}
{% endfor %}
{% endmacro %}
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
{{accessModifier}} enum {{enumName}} {
{% if catalogs.count > 1 or param.forceFileNameEnum %}
{% for catalog in catalogs %}
{{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call enumBlock catalog.assets %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call enumBlock catalogs.first.assets %}
{% endif %}
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
// MARK: - Implementation Details
{% if resourceCount.arresourcegroup > 0 %}
{{accessModifier}} struct {{arResourceGroupType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(iOS)
@available(iOS 11.3, *)
{{accessModifier}} var referenceImages: Set<ARReferenceImage> {
return ARReferenceImage.referenceImages(in: self)
}
@available(iOS 12.0, *)
{{accessModifier}} var referenceObjects: Set<ARReferenceObject> {
return ARReferenceObject.referenceObjects(in: self)
}
#endif
}
#if os(iOS)
@available(iOS 11.3, *)
{{accessModifier}} extension ARReferenceImage {
static func referenceImages(in asset: {{arResourceGroupType}}) -> Set<ARReferenceImage> {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set()
}
}
@available(iOS 12.0, *)
{{accessModifier}} extension ARReferenceObject {
static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set<ARReferenceObject> {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set()
}
}
#endif
{% endif %}
{% if resourceCount.color > 0 %}
{{accessModifier}} final class {{colorType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(macOS)
{{accessModifier}} typealias Color = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Color = UIColor
#endif
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
{{accessModifier}} private(set) lazy var color: Color = Color(asset: self)
#if os(iOS) || os(tvOS)
@available(iOS 11.0, tvOS 11.0, *)
{{accessModifier}} func color(compatibleWith traitCollection: UITraitCollection) -> Color {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
fatalError("Unable to load color asset named \(name).")
}
return color
}
#endif
fileprivate init(name: String) {
self.name = name
}
}
{{accessModifier}} extension {{colorType}}.Color {
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
convenience init!(asset: {{colorType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
self.init(named: NSColor.Name(asset.name), bundle: bundle)
#elseif os(watchOS)
self.init(named: asset.name)
#endif
}
}
{% endif %}
{% if resourceCount.data > 0 %}
{{accessModifier}} struct {{dataType}} {
{{accessModifier}} fileprivate(set) var name: String
@available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
{{accessModifier}} var data: NSDataAsset {
return NSDataAsset(asset: self)
}
}
@available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
{{accessModifier}} extension NSDataAsset {
convenience init!(asset: {{dataType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS) || os(watchOS)
self.init(name: asset.name, bundle: bundle)
#elseif os(macOS)
self.init(name: NSDataAsset.Name(asset.name), bundle: bundle)
#endif
}
}
{% endif %}
{% if resourceCount.image > 0 %}
{{accessModifier}} struct {{imageType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(macOS)
{{accessModifier}} typealias Image = NSImage
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Image = UIImage
#endif
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
{{accessModifier}} var image: Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
let image = Image(named: name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
let name = NSImage.Name(self.name)
let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
#elseif os(watchOS)
let image = Image(named: name)
#endif
guard let result = image else {
fatalError("Unable to load image asset named \(name).")
}
return result
}
#if os(iOS) || os(tvOS)
@available(iOS 8.0, tvOS 9.0, *)
{{accessModifier}} func image(compatibleWith traitCollection: UITraitCollection) -> Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
fatalError("Unable to load image asset named \(name).")
}
return result
}
#endif
}
{{accessModifier}} extension {{imageType}}.Image {
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
@available(macOS, deprecated,
message: "This initializer is unsafe on macOS, please use the {{imageType}}.image property")
convenience init!(asset: {{imageType}}) {
#if os(iOS) || os(tvOS)
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
self.init(named: NSImage.Name(asset.name))
#elseif os(watchOS)
self.init(named: asset.name)
#endif
}
}
{% endif %}
{% if resourceCount.symbol > 0 %}
{{accessModifier}} struct {{symbolType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(iOS) || os(tvOS) || os(watchOS)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
{{accessModifier}} typealias Configuration = UIImage.SymbolConfiguration
{{accessModifier}} typealias Image = UIImage
@available(iOS 12.0, tvOS 12.0, watchOS 5.0, *)
{{accessModifier}} var image: Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
let image = Image(named: name, in: bundle, compatibleWith: nil)
#elseif os(watchOS)
let image = Image(named: name)
#endif
guard let result = image else {
fatalError("Unable to load symbol asset named \(name).")
}
return result
}
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
{{accessModifier}} func image(with configuration: Configuration) -> Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let result = Image(named: name, in: bundle, with: configuration) else {
fatalError("Unable to load symbol asset named \(name).")
}
return result
}
#endif
}
{% endif %}
{% if not param.bundle %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No assets found
{% endif %}

View file

@ -0,0 +1,337 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if catalogs %}
{% set enumName %}{{param.enumName|default:"Asset"}}{% endset %}
{% set arResourceGroupType %}{{param.arResourceGroupTypeName|default:"ARResourceGroupAsset"}}{% endset %}
{% set colorType %}{{param.colorTypeName|default:"ColorAsset"}}{% endset %}
{% set dataType %}{{param.dataTypeName|default:"DataAsset"}}{% endset %}
{% set imageType %}{{param.imageTypeName|default:"ImageAsset"}}{% endset %}
{% set symbolType %}{{param.symbolTypeName|default:"SymbolAsset"}}{% endset %}
{% set forceNamespaces %}{{param.forceProvidesNamespaces|default:"false"}}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
#if os(macOS)
import AppKit
#elseif os(iOS)
{% if resourceCount.arresourcegroup > 0 %}
import ARKit
{% endif %}
import UIKit
#elseif os(tvOS) || os(watchOS)
import UIKit
#endif
// Deprecated typealiases
{% if resourceCount.color > 0 %}
@available(*, deprecated, renamed: "{{colorType}}.Color", message: "This typealias will be removed in SwiftGen 7.0")
{{accessModifier}} typealias {{param.colorAliasName|default:"AssetColorTypeAlias"}} = {{colorType}}.Color
{% endif %}
{% if resourceCount.image > 0 %}
@available(*, deprecated, renamed: "{{imageType}}.Image", message: "This typealias will be removed in SwiftGen 7.0")
{{accessModifier}} typealias {{param.imageAliasName|default:"AssetImageTypeAlias"}} = {{imageType}}.Image
{% endif %}
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Asset Catalogs
{% macro enumBlock assets %}
{% call casesBlock assets %}
{% if param.allValues %}
// swiftlint:disable trailing_comma
{% if resourceCount.arresourcegroup > 0 %}
{{accessModifier}} static let allResourceGroups: [{{arResourceGroupType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "arresourcegroup" "" %}{% endfilter %}
]
{% endif %}
{% if resourceCount.color > 0 %}
{{accessModifier}} static let allColors: [{{colorType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "color" "" %}{% endfilter %}
]
{% endif %}
{% if resourceCount.data > 0 %}
{{accessModifier}} static let allDataAssets: [{{dataType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "data" "" %}{% endfilter %}
]
{% endif %}
{% if resourceCount.image > 0 %}
{{accessModifier}} static let allImages: [{{imageType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "image" "" %}{% endfilter %}
]
{% endif %}
{% if resourceCount.symbol > 0 %}
{{accessModifier}} static let allSymbols: [{{symbolType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "symbol" "" %}{% endfilter %}
]
{% endif %}
// swiftlint:enable trailing_comma
{% endif %}
{% endmacro %}
{% macro casesBlock assets %}
{% for asset in assets %}
{% if asset.type == "arresourcegroup" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{arResourceGroupType}}(name: "{{asset.value}}")
{% elif asset.type == "color" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{colorType}}(name: "{{asset.value}}")
{% elif asset.type == "data" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{dataType}}(name: "{{asset.value}}")
{% elif asset.type == "image" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}")
{% elif asset.type == "symbol" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{symbolType}}(name: "{{asset.value}}")
{% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
{{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
}
{% elif asset.items %}
{% call casesBlock asset.items %}
{% endif %}
{% endfor %}
{% endmacro %}
{% macro allValuesBlock assets filter prefix %}
{% for asset in assets %}
{% if asset.type == filter %}
{{prefix}}{{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}},
{% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
{% set prefix2 %}{{prefix}}{{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.{% endset %}
{% call allValuesBlock asset.items filter prefix2 %}
{% elif asset.items %}
{% call allValuesBlock asset.items filter prefix %}
{% endif %}
{% endfor %}
{% endmacro %}
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
{{accessModifier}} enum {{enumName}} {
{% if catalogs.count > 1 or param.forceFileNameEnum %}
{% for catalog in catalogs %}
{{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call enumBlock catalog.assets %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call enumBlock catalogs.first.assets %}
{% endif %}
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
// MARK: - Implementation Details
{% if resourceCount.arresourcegroup > 0 %}
{{accessModifier}} struct {{arResourceGroupType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(iOS)
@available(iOS 11.3, *)
{{accessModifier}} var referenceImages: Set<ARReferenceImage> {
return ARReferenceImage.referenceImages(in: self)
}
@available(iOS 12.0, *)
{{accessModifier}} var referenceObjects: Set<ARReferenceObject> {
return ARReferenceObject.referenceObjects(in: self)
}
#endif
}
#if os(iOS)
@available(iOS 11.3, *)
{{accessModifier}} extension ARReferenceImage {
static func referenceImages(in asset: {{arResourceGroupType}}) -> Set<ARReferenceImage> {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set()
}
}
@available(iOS 12.0, *)
{{accessModifier}} extension ARReferenceObject {
static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set<ARReferenceObject> {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set()
}
}
#endif
{% endif %}
{% if resourceCount.color > 0 %}
{{accessModifier}} final class {{colorType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(macOS)
{{accessModifier}} typealias Color = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Color = UIColor
#endif
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
{{accessModifier}} private(set) lazy var color: Color = {
guard let color = Color(asset: self) else {
fatalError("Unable to load color asset named \(name).")
}
return color
}()
#if os(iOS) || os(tvOS)
@available(iOS 11.0, tvOS 11.0, *)
{{accessModifier}} func color(compatibleWith traitCollection: UITraitCollection) -> Color {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
fatalError("Unable to load color asset named \(name).")
}
return color
}
#endif
fileprivate init(name: String) {
self.name = name
}
}
{{accessModifier}} extension {{colorType}}.Color {
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
convenience init?(asset: {{colorType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
self.init(named: NSColor.Name(asset.name), bundle: bundle)
#elseif os(watchOS)
self.init(named: asset.name)
#endif
}
}
{% endif %}
{% if resourceCount.data > 0 %}
{{accessModifier}} struct {{dataType}} {
{{accessModifier}} fileprivate(set) var name: String
@available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
{{accessModifier}} var data: NSDataAsset {
guard let data = NSDataAsset(asset: self) else {
fatalError("Unable to load data asset named \(name).")
}
return data
}
}
@available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
{{accessModifier}} extension NSDataAsset {
convenience init?(asset: {{dataType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS) || os(watchOS)
self.init(name: asset.name, bundle: bundle)
#elseif os(macOS)
self.init(name: NSDataAsset.Name(asset.name), bundle: bundle)
#endif
}
}
{% endif %}
{% if resourceCount.image > 0 %}
{{accessModifier}} struct {{imageType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(macOS)
{{accessModifier}} typealias Image = NSImage
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Image = UIImage
#endif
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
{{accessModifier}} var image: Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
let image = Image(named: name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
let name = NSImage.Name(self.name)
let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
#elseif os(watchOS)
let image = Image(named: name)
#endif
guard let result = image else {
fatalError("Unable to load image asset named \(name).")
}
return result
}
#if os(iOS) || os(tvOS)
@available(iOS 8.0, tvOS 9.0, *)
{{accessModifier}} func image(compatibleWith traitCollection: UITraitCollection) -> Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
fatalError("Unable to load image asset named \(name).")
}
return result
}
#endif
}
{{accessModifier}} extension {{imageType}}.Image {
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
@available(macOS, deprecated,
message: "This initializer is unsafe on macOS, please use the {{imageType}}.image property")
convenience init?(asset: {{imageType}}) {
#if os(iOS) || os(tvOS)
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
self.init(named: NSImage.Name(asset.name))
#elseif os(watchOS)
self.init(named: asset.name)
#endif
}
}
{% endif %}
{% if resourceCount.symbol > 0 %}
{{accessModifier}} struct {{symbolType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(iOS) || os(tvOS) || os(watchOS)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
{{accessModifier}} typealias Configuration = UIImage.SymbolConfiguration
{{accessModifier}} typealias Image = UIImage
@available(iOS 12.0, tvOS 12.0, watchOS 5.0, *)
{{accessModifier}} var image: Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
let image = Image(named: name, in: bundle, compatibleWith: nil)
#elseif os(watchOS)
let image = Image(named: name)
#endif
guard let result = image else {
fatalError("Unable to load symbol asset named \(name).")
}
return result
}
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
{{accessModifier}} func image(with configuration: Configuration) -> Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let result = Image(named: name, in: bundle, with: configuration) else {
fatalError("Unable to load symbol asset named \(name).")
}
return result
}
#endif
}
{% endif %}
{% if not param.bundle %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No assets found
{% endif %}

View file

@ -0,0 +1,92 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set documentPrefix %}{{param.documentName|default:"Document"}}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - YAML Files
{% macro fileBlock file %}
{% if file.documents.count > 1 %}
{% for document in file.documents %}
{% set documentName %}{{documentPrefix}}{{forloop.counter}}{% endset %}
{{accessModifier}} enum {{documentName|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call documentBlock file document %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call documentBlock file file.documents.first %}
{% endif %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% elif document.metadata.type == "Dictionary" %}
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value document.data %}
{% endfor %}
{% else %}
{{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% elif metadata.type == "Optional" %}
Any?
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
{% endfilter %}{% endmacro %}
{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "String" %}
"{{ value }}"
{% elif metadata.type == "Optional" %}
nil
{% elif metadata.type == "Array" and value %}
[{% for value in value %}
{% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
{{ ", " if not forloop.last }}
{% endfor %}]
{% elif metadata.type == "Dictionary" %}
[{% for key,value in value %}
"{{key}}": {% call valueBlock value metadata.properties[key] %}
{{ ", " if not forloop.last }}
{% empty %}
:
{% endfor %}]
{% elif metadata.type == "Bool" %}
{% if value %}true{% else %}false{% endif %}
{% else %}
{{ value }}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length number_separator type_body_length
{{accessModifier}} enum {{param.enumName|default:"YAMLFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length number_separator type_body_length
{% else %}
// No files found
{% endif %}

View file

@ -0,0 +1,92 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if files %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set documentPrefix %}{{param.documentName|default:"Document"}}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - YAML Files
{% macro fileBlock file %}
{% if file.documents.count > 1 %}
{% for document in file.documents %}
{% set documentName %}{{documentPrefix}}{{forloop.counter}}{% endset %}
{{accessModifier}} enum {{documentName|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call documentBlock file document %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call documentBlock file file.documents.first %}
{% endif %}
{% endmacro %}
{% macro documentBlock file document %}
{% set rootType %}{% call typeBlock document.metadata %}{% endset %}
{% if document.metadata.type == "Array" %}
{{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% elif document.metadata.type == "Dictionary" %}
{% for key,value in document.metadata.properties %}
{{accessModifier}} {% call propertyBlock key value document.data %}
{% endfor %}
{% else %}
{{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
{% endif %}
{% endmacro %}
{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "Array" %}
[{% call typeBlock metadata.element %}]
{% elif metadata.type == "Dictionary" %}
[String: Any]
{% elif metadata.type == "Optional" %}
Any?
{% else %}
{{metadata.type}}
{% endif %}
{% endfilter %}{% endmacro %}
{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
{% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set propertyType %}{% call typeBlock metadata %}{% endset %}
static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
{% endfilter %}{% endmacro %}
{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
{% if metadata.type == "String" %}
"{{ value }}"
{% elif metadata.type == "Optional" %}
nil
{% elif metadata.type == "Array" and value %}
[{% for value in value %}
{% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
{{ ", " if not forloop.last }}
{% endfor %}]
{% elif metadata.type == "Dictionary" %}
[{% for key,value in value %}
"{{key}}": {% call valueBlock value metadata.properties[key] %}
{{ ", " if not forloop.last }}
{% empty %}
:
{% endfor %}]
{% elif metadata.type == "Bool" %}
{% if value %}true{% else %}false{% endif %}
{% else %}
{{ value }}
{% endif %}
{% endfilter %}{% endmacro %}
// swiftlint:disable identifier_name line_length number_separator type_body_length
{{accessModifier}} enum {{param.enumName|default:"YAMLFiles"}} {
{% if files.count > 1 or param.forceFileNameEnum %}
{% for file in files %}
{{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call fileBlock files.first %}
{% endif %}
}
// swiftlint:enable identifier_name line_length number_separator type_body_length
{% else %}
// No files found
{% endif %}

BIN
Monal/.swiftgen/bin/swiftgen Executable file

Binary file not shown.

View file

@ -0,0 +1,29 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if families %}
import SwiftUI
{% for family in families %}
{% set identifierName %}{{family.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
{% set styleTypeName %}{{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}Style{% endset %}
extension Font {
public static func {{identifierName}}(_ style: {{styleTypeName}}, fixedSize: CGFloat) -> Font {
return Font.custom(style.rawValue, fixedSize: fixedSize)
}
public static func {{identifierName}}(_ style: {{styleTypeName}}, size: CGFloat, relativeTo textStyle: TextStyle = .body) -> Font {
return Font.custom(style.rawValue, size: size, relativeTo: textStyle)
}
public enum {{styleTypeName}}: String {
{% for font in family.fonts %}
case {{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = "{{font.name}}"
{% endfor %}
}
}
{% endfor %}
{% else %}
// No fonts found
{% endif %}
// swiftlint:enable all

View file

@ -0,0 +1,85 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if tables.count > 0 %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
import Foundation
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Strings
{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
_ p{{forloop.counter}}: Any
{% else %}
_ p{{forloop.counter}}: {{type}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
{% for type in types %}
{% if type == "String" %}
String(describing: p{{forloop.counter}})
{% elif type == "UnsafeRawPointer" %}
Int(bitPattern: p{{forloop.counter}})
{% else %}
p{{forloop.counter}}
{% endif %}
{{ ", " if not forloop.last }}
{% endfor %}
{% endfilter %}{% endmacro %}
{% macro recursiveBlock table item %}
{% for string in item.strings %}
{% if not param.noComments %}
{% for line in string.translation|split:"\n" %}
/// {{line}}
{% endfor %}
{% endif %}
{% if string.types %}
{{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
{{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
}
{% else %}
{{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { {{enumName}}.tr("{{table}}", "{{string.key}}") }
{% endif %}
{% endfor %}
{% for child in item.children %}
{{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %}
}
{% endfor %}
{% endmacro %}
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
{{accessModifier}} enum {{enumName}} {
{% if tables.count > 1 or param.forceFileNameEnum %}
{% for table in tables %}
{{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call recursiveBlock tables.first.name tables.first.levels %}
{% endif %}
}
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
import Localize_Swift
extension {{enumName}} {
static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
let selectedLanguage = Localize.currentLanguage()
guard let path = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj"),
let bundle = Bundle(path: path) else { return "Setup language error" }
return NSLocalizedString(key, tableName: table, bundle: bundle, comment: "")
}
}
{% endif %}
// swiftlint: enable all

View file

@ -0,0 +1,48 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if catalogs %}
import SwiftUI
{% macro casesBlock assets %}
{% for asset in assets %}
{% if asset.items and asset.isNamespaced == "true" %}
public enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
}
{% elif asset.items %}
{% call casesBlock asset.items %}
{% elif asset.type == "color" %}
public static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = Color("{{asset.value}}")
{% elif asset.type == "image" %}
public static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = Image("{{asset.value}}")
{% endif %}
{% endfor %}
{% endmacro %}
{% for catalog in catalogs %}
{% if catalog.name == "Colors" %}
extension Color {
{% for catalog in catalogs %}
{% if catalog.name == "Colors" %}
{% call casesBlock catalog.assets %}
{% endif %}
{% endfor %}
}
{% endif %}
{% endfor %}
{% for catalog in catalogs %}
{% if catalog.name == "Images" %}
extension Image {
{% for catalog in catalogs %}
{% if catalog.name == "Images" %}
{% call casesBlock catalog.assets %}
{% endif %}
{% endfor %}
}
{% endif %}
{% endfor %}
{% else %}
// No assets found
{% endif %}
// swiftlint: enable all

View file

@ -0,0 +1,36 @@
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if catalogs %}
import Foundation
typealias AssetStrings = String
{% macro casesBlock assets %}
{% for asset in assets %}
{% if asset.items and asset.isNamespaced == "true" %}
public enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
}
{% elif asset.items %}
{% call casesBlock asset.items %}
{% elif asset.type == "image" %}
public static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = String("{{asset.value}}")
{% endif %}
{% endfor %}
{% endmacro %}
{% for catalog in catalogs %}
{% if catalog.name == "Images" %}
extension String {
{% for catalog in catalogs %}
{% if catalog.name == "Images" %}
{% call casesBlock catalog.assets %}
{% endif %}
{% endfor %}
}
{% endif %}
{% endfor %}
{% else %}
// No assets found
{% endif %}
// swiftlint: enable all

125
Monal/.swiftlint.yml Normal file
View file

@ -0,0 +1,125 @@
---
colon:
severity: error
line_length:
ignores_comments: true
warning: 260
error: 300
type_body_length:
warning: 300
error: 500
file_length:
warning: 800
error: 1000
function_parameter_count:
warning: 20
error: 30
function_body_length:
warning: 120
error: 150
cyclomatic_complexity:
warning: 40
error: 50
nesting:
type_level:
warning: 3
error: 6
function_level:
warning: 500
error: 10
vertical_parameter_alignment:
severity: warning
implicitly_unwrapped_optional:
severity: warning
force_unwrapping:
severity: error
vertical_whitespace:
severity: error
force_try:
severity: error
trailing_semicolon:
severity: error
type_name:
min_length:
warninig: 3
error: 0
max_length:
warninig: 40
error: 80
identifier_name:
min_length: 3
max_length: 60
# validates_start_with_lowercase: true
allowed_symbols: "_"
excluded:
- iv
- id
- ip
- on
- ui
- x
- y
- tz
- to
- db
- _db
# Disable rules from the default enabled set.
disabled_rules:
- trailing_whitespace
- implicit_getter
- redundant_string_enum_value
- switch_case_alignment
# Enable rules not from the default set.
opt_in_rules:
# - function_default_parameter_at_end
- empty_count
- indentation_width
# - index_at_zero
- legacy_constant
# - implicitly_unwrapped_optional
- force_unwrapping
# - no header
- file_header
# - for force unwrapping
- implicitly_unwrapped_optional
- vertical_parameter_alignment_on_call
- vertical_whitespace_between_cases
- vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces
# Acts as a whitelist, only the rules specified in this list will be enabled. Can not be specified alongside disabled_rules or opt_in_rules.
only_rules:
# This is an entirely separate list of rules that are only run by the analyze command. All analyzer rules are opt-in, so this is the only configurable rule list (there is no disabled/whitelist equivalent).
analyzer_rules:
- unused_import
- unused_declaration
unused_declaration:
include_public_and_open: true
# paths to ignore during linting. Takes precedence over `included`.
excluded:
- .swiftgen
- "**/Generated"
- Classes
- Pods
- MonalUITests
- MonalXMPPUnitTests

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.monalalpha</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

BIN
Monal/CallSounds/busy.wav Normal file

Binary file not shown.

BIN
Monal/CallSounds/error.wav Normal file

Binary file not shown.

Binary file not shown.

26
Monal/Classes/AESGcm.h Normal file
View file

@ -0,0 +1,26 @@
//
// AESGcm.h
// Monal
//
// Created by Anurodh Pokharel on 4/19/19.
// Copyright © 2019 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLEncryptedPayload.h"
NS_ASSUME_NONNULL_BEGIN
@interface AESGcm : NSObject
/**
key size should be 16 or 32
*/
+(MLEncryptedPayload* _Nullable) encrypt:(NSData*) body keySize:(int) keySize;
+(MLEncryptedPayload* _Nullable) encrypt:(NSData*) body withKey:(NSData*) gcmKey;
+(NSData* _Nullable) decrypt:(NSData *)body withKey:(NSData *) key andIv:(NSData *)iv withAuth:(NSData * _Nullable) auth;
+(NSData* _Nullable) genIV;
+(NSData* _Nullable) genKey:(int) keySize;
@end
NS_ASSUME_NONNULL_END

71
Monal/Classes/AESGcm.m Normal file
View file

@ -0,0 +1,71 @@
//
// AESGcm.m
// Monal
//
// Created by Anurodh Pokharel on 4/19/19.
// Copyright © 2019 Monal.im. All rights reserved.
//
#import "MLConstants.h"
#import "AESGcm.h"
#import <monalxmpp/monalxmpp-Swift.h>
@class MLCrypto;
@implementation AESGcm
+(MLEncryptedPayload*) encrypt:(NSData*) body keySize:(int) keySize
{
NSData* gcmKey = [self genKey:keySize];
if(!gcmKey)
{
return nil;
}
return [self encrypt:body withKey:gcmKey];
}
+(MLEncryptedPayload*) encrypt:(NSData*) body withKey:(NSData*) gcmKey
{
MLCrypto* crypto = [MLCrypto new];
EncryptedPayload* payload = [crypto encryptGCMWithKey:gcmKey decryptedContent:body];
if(payload == nil)
{
return nil;
}
NSMutableData* combinedKey = [NSMutableData dataWithData:gcmKey];
[combinedKey appendData:payload.tag];
if(combinedKey == nil)
{
return nil;
}
return [[MLEncryptedPayload alloc] initWithBody:payload.body key:combinedKey iv:payload.iv authTag:payload.tag];
}
+(NSData*) genIV
{
MLCrypto* crypto = [MLCrypto new];
return [crypto genIV];
}
+(NSData*) genKey:(int) keySize
{
uint8_t randomBytes[keySize];
if(SecRandomCopyBytes(kSecRandomDefault, keySize, randomBytes) != 0)
return nil;
return [[NSData alloc] initWithBytes:randomBytes length:keySize];
}
+(NSData*) decrypt:(NSData*) body withKey:(NSData*) key andIv:(NSData*) iv withAuth:(NSData* _Nullable) auth
{
MLCrypto* crypto = [MLCrypto new];
NSMutableData* combined = [NSMutableData new];
[combined appendData:iv];
[combined appendData:body];
[combined appendData:auth]; //if auth is nil assume it already was apended to body
NSData* toReturn = [crypto decryptGCMWithKey:key encryptedContent:combined];
return toReturn;
}
@end

View file

@ -0,0 +1,594 @@
//
// AVCallUI.swift
// Monal
//
// Created by Thilo Molitor on 20.12.22.
// Copyright © 2021 Monal.im. All rights reserved.
//
import WebRTC
import AVFoundation
import CallKit
import AVKit
struct VideoView: UIViewRepresentable {
var renderer: RTCMTLVideoView
init(renderer: RTCMTLVideoView) {
self.renderer = renderer
}
func makeUIView(context: Context) -> RTCMTLVideoView {
return self.renderer
}
func updateUIView(_ renderer: RTCMTLVideoView, context: Context) {
DDLogDebug("updateUIView called...")
//do nothing
}
}
struct AVCallUI: View {
@StateObject private var appDelegate: ObservableKVOWrapper<MonalAppDelegate>
@StateObject private var call: ObservableKVOWrapper<MLCall>
@StateObject private var contact: ObservableKVOWrapper<MLContact>
@State private var showMicAlert = false
@State private var showSecurityHelpAlert: MLCallEncryptionState? = nil
@State private var controlsVisible = true
@State private var localRendererLocation: CGPoint = CGPoint(
x: UIScreen.main.bounds.size.width - (UIScreen.main.bounds.size.width/5.0/2.0 + 24.0),
y: UIScreen.main.bounds.size.height/5.0/2.0 + 16.0
)
@State private var cameraPosition: AVCaptureDevice.Position = .front
@State private var sendingVideo = true
private var ringingPlayer: AVAudioPlayer!
private var busyPlayer: AVAudioPlayer!
private var errorPlayer: AVAudioPlayer!
private var delegate: SheetDismisserProtocol
private var formatter: DateComponentsFormatter
private var localRenderer: RTCMTLVideoView
private var remoteRenderer: RTCMTLVideoView
init(delegate: SheetDismisserProtocol, call: MLCall) {
_call = StateObject(wrappedValue: ObservableKVOWrapper(call))
_contact = StateObject(wrappedValue: ObservableKVOWrapper(call.contact))
_appDelegate = StateObject(wrappedValue: ObservableKVOWrapper(UIApplication.shared.delegate as! MonalAppDelegate))
self.delegate = delegate
self.formatter = DateComponentsFormatter()
self.formatter.allowedUnits = [.hour, .minute, .second]
self.formatter.unitsStyle = .positional
self.formatter.zeroFormattingBehavior = .pad
//use the complete screen for remote video
self.remoteRenderer = RTCMTLVideoView(frame: UIScreen.main.bounds)
self.remoteRenderer.videoContentMode = .scaleAspectFill
self.localRenderer = RTCMTLVideoView(frame: UIScreen.main.bounds)
self.localRenderer.videoContentMode = .scaleAspectFill
self.localRenderer.transform = CGAffineTransformMakeScale(-1.0, 1.0) //local video should be displayed as "mirrored"
self.ringingPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"ringing", withExtension:"wav", subdirectory:"CallSounds")!)
self.busyPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"busy", withExtension:"wav", subdirectory:"CallSounds")!)
self.errorPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"error", withExtension:"wav", subdirectory:"CallSounds")!)
}
func maybeStartRenderer() {
if MLCallType(rawValue:call.callType) == .video && MLCallState(rawValue:call.state) == .connected {
DDLogInfo("Starting local and remote video renderers...")
call.obj.startCaptureLocalVideo(withRenderer: self.localRenderer, andCameraPosition:cameraPosition)
call.obj.renderRemoteVideo(withRenderer: self.remoteRenderer)
}
}
func handleStateChange(_ state:MLCallState, _ audioState:MLAudioState) {
switch state {
case .unknown:
DDLogDebug("state: unknown")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.play()
case .discovering:
DDLogDebug("state: discovering")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
case .ringing:
DDLogDebug("state: ringing")
busyPlayer.stop()
errorPlayer.stop()
ringingPlayer.play()
case .connecting:
DDLogDebug("state: connecting")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
case .reconnecting:
DDLogDebug("state: reconnecting")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
case .connected:
DDLogDebug("state: connected")
maybeStartRenderer()
case .finished:
DDLogDebug("state: finished: \(String(describing:call.finishReason as NSNumber))")
//check audio state before trying to play anything (if we are still in state .call,
//callkit will deactivate this audio session shortly, stopping our players)
if audioState == .normal {
switch MLCallFinishReason(rawValue:call.finishReason) {
case .unknown:
DDLogDebug("state: finished: unknown")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
case .connectivityError:
DDLogDebug("state: finished: connectivityError")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.play()
case .securityError:
DDLogDebug("state: finished: securityError")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.play()
case .unanswered:
DDLogDebug("state: finished: unanswered")
ringingPlayer.stop()
errorPlayer.stop()
busyPlayer.play()
case .retracted:
DDLogDebug("state: finished: retracted")
ringingPlayer.stop()
errorPlayer.stop()
busyPlayer.play()
case .rejected:
DDLogDebug("state: finished: rejected")
ringingPlayer.stop()
errorPlayer.stop()
busyPlayer.play()
case .declined:
DDLogDebug("state: finished: declined")
ringingPlayer.stop()
errorPlayer.stop()
busyPlayer.play()
case .error:
DDLogDebug("state: finished: error")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.play()
// case .normal:
// case .answeredElsewhere:
default:
DDLogDebug("state: finished: default")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
}
}
default:
DDLogDebug("state: default")
}
}
var body: some View {
ZStack {
Color.background
.ignoresSafeArea()
if MLCallType(rawValue:call.callType) == .video && MLCallState(rawValue:call.state) == .connected {
VideoView(renderer:self.remoteRenderer)
ZStack {
VideoView(renderer:self.localRenderer)
//this will sometimes only honor the width and ignore the height
.frame(width: UIScreen.main.bounds.size.width/5.0, height: UIScreen.main.bounds.size.height/5.0)
if controlsVisible {
Button(action: {
if cameraPosition == .front {
cameraPosition = .back
} else {
cameraPosition = .front
}
call.obj.stopCaptureLocalVideo()
maybeStartRenderer()
}, label: {
Image(systemName: "arrow.triangle.2.circlepath.camera.fill")
.resizable()
.frame(width: 32.0, height: 32.0)
.foregroundColor(.primary)
})
}
}
.position(localRendererLocation)
.gesture(DragGesture().onChanged { value in
self.localRendererLocation = value.location
})
.onTapGesture(count: 2) {
if sendingVideo {
call.obj.hideVideo()
} else {
call.obj.showVideo()
}
sendingVideo = !sendingVideo
}
}
if MLCallType(rawValue:call.callType) == .audio ||
(MLCallType(rawValue:call.callType) == .video && (MLCallState(rawValue:call.state) != .connected || controlsVisible)) {
VStack {
Group {
Spacer().frame(height: 24)
HStack(alignment: .top) {
Spacer().frame(width:20)
VStack {
Spacer().frame(height: 8)
switch MLCallDirection(rawValue:call.direction) {
case .incoming:
Image(systemName: "phone.arrow.down.left")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.primary)
case .outgoing:
Image(systemName: "phone.arrow.up.right")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.primary)
default: //should never be reached
Text("")
}
}
VStack {
Spacer().frame(height: 8)
Button(action: {
//show dialog explaining different encryption states
self.showSecurityHelpAlert = MLCallEncryptionState(rawValue:call.encryptionState)
}, label: {
switch MLCallEncryptionState(rawValue:call.encryptionState) {
case .unknown:
Text("")
case .clear:
Spacer().frame(width: 10)
Image(systemName: "xmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.red)
case .toFU:
Spacer().frame(width: 10)
Image(systemName: "checkmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.yellow)
case .trusted:
Spacer().frame(width: 10)
Image(systemName: "checkmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.green)
default: //should never be reached
Text("")
}
})
}
Spacer()
Text(contact.contactDisplayName as String)
.font(.largeTitle)
.foregroundColor(.primary)
Spacer()
VStack {
Spacer().frame(height: 8)
Button(action: {
if let activeChats = self.appDelegate.obj.activeChats {
//make sure we don't animate anything
activeChats.dismissCompleteViewChain(withAnimation: false) {
activeChats.presentChat(with:self.contact.obj)
}
} else {
//self.delegate.dismissWithoutAnimation()
unreachable("active chats should always be accessible from AVCallUI!")
}
}, label: {
Image(systemName: "text.bubble")
.resizable()
.frame(width: 28.0, height: 28.0)
.foregroundColor(.primary)
})
}
Spacer().frame(width:20)
}
Spacer().frame(height: 16)
//this is needed because ObservableKVOWrapper somehow extracts an NSNumber? from it's wrapped object
//which results in a runtime error when trying to cast NSNumber? to MLCallState
switch MLCallState(rawValue:call.state) {
case .discovering:
Text("Discovering devices...")
.bold()
.foregroundColor(.primary)
case .ringing:
Text("Ringing...")
.bold()
.foregroundColor(.primary)
case .connecting:
Text("Connecting...")
.bold()
.foregroundColor(.primary)
case .reconnecting:
Text("Reconnecting...")
.bold()
.foregroundColor(.primary)
case .connected:
Text("Connected: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
.bold()
.foregroundColor(.primary)
case .finished:
switch MLCallFinishReason(rawValue:call.finishReason) {
case .unknown:
Text("Call ended for an unknown reason")
.bold()
.foregroundColor(.primary)
case .normal:
if call.wasConnectedOnce {
Text("Call ended, duration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
.bold()
.foregroundColor(.primary)
} else {
Text("Call ended")
.bold()
.foregroundColor(.primary)
}
case .connectivityError:
if call.wasConnectedOnce {
Text("Call ended: connection failed\nDuration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
.bold()
.foregroundColor(.primary)
} else {
Text("Call ended: connection failed")
.bold()
.foregroundColor(.primary)
}
case .securityError:
Text("Call ended: couldn't establish encryption")
.bold()
.foregroundColor(.primary)
case .unanswered:
Text("Call was not answered")
.bold()
.foregroundColor(.primary)
case .answeredElsewhere:
Text("Call ended: answered with other device")
.bold()
.foregroundColor(.primary)
case .retracted:
//this will only be displayed for timer-induced retractions,
//reflect that in our text instead of using some generic "hung up"
//Text("Call ended: hung up")
Text("Call ended: remote busy")
.bold()
.foregroundColor(.primary)
case .rejected:
Text("Call ended: remote busy")
.bold()
.foregroundColor(.primary)
case .declined:
Text("Call ended: declined")
.bold()
.foregroundColor(.primary)
case .error:
Text("Call ended: application error")
.bold()
.foregroundColor(.primary)
default: //should never be reached
Text("")
}
default: //should never be reached
Text("")
}
Spacer().frame(height: 48)
if MLCallType(rawValue:call.callType) == .audio || MLCallState(rawValue:call.state) != .connected {
Image(uiImage: contact.avatar)
.resizable()
.frame(minWidth: 100, idealWidth: 150, maxWidth: 200, minHeight: 100, idealHeight: 150, maxHeight: 200, alignment: .center)
.scaledToFit()
.shadow(radius: 7)
}
Spacer()
}
if MLCallState(rawValue:call.state) == .finished {
HStack() {
Spacer()
Button(action: {
self.delegate.dismissWithoutAnimation()
if let activeChats = self.appDelegate.obj.activeChats {
activeChats.call(contact.obj, with:MLCallType(rawValue:call.callType)!)
}
}) {
Image(systemName: "arrow.clockwise.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .green)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
Spacer().frame(width: 64)
Button(action: {
delegate.dismissWithoutAnimation()
}) {
Image(systemName: "x.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
Spacer()
}
} else {
HStack() {
Spacer()
if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting {
Button(action: {
call.muted = !call.muted
}) {
Image(systemName: "mic.slash.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(call.muted ? .black : .white, call.muted ? .white : .black)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
Spacer().frame(width: 32)
}
Button(action: {
call.obj.end()
self.delegate.dismissWithoutAnimation()
}) {
Image(systemName: "phone.down.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting {
Spacer().frame(width: 32)
Button(action: {
call.speaker = !call.speaker
}) {
Image(systemName: "speaker.wave.2.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(call.speaker ? .black : .white, call.speaker ? .white : .black)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
}
Spacer()
}
}
Spacer().frame(height: 32)
}
}
}
.onTapGesture(count: 1) {
controlsVisible = !controlsVisible
}
.alert(isPresented: $showMicAlert) {
Alert(
title: Text("Missing permission"),
message: Text("You need to grant microphone access in iOS Settings-> Privacy-> Microphone, if you want that others can hear you."),
dismissButton: .default(Text("OK"))
)
}
.richAlert(isPresented:$showSecurityHelpAlert, title:Text("Call security help")) {
VStack(alignment: .leading) {
HStack {
Image(systemName: "xmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.red)
Spacer().frame(width: 10)
Text("Red x-mark shield:")
}.font(Font.body.weight(showSecurityHelpAlert == .clear ? .heavy : .medium))
Text("This means your call is encrypted, but the remote party could not be verified using OMEMO encryption.\nYour or the callee's XMPP server could possibly Man-In-The-Middle you.")
Spacer().frame(height: 20)
HStack {
Image(systemName: "checkmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.yellow)
Spacer().frame(width: 10)
Text("Yellow checkmark shield:")
}.font(Font.body.weight(showSecurityHelpAlert == .toFU ? .heavy : .medium))
Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nBut since you did not manually verify the callee's OMEMO fingerprints, your or the callee's XMPP server could possibly have inserted their own OMEMO keys to Man-In-The-Middle you.")
Spacer().frame(height: 20)
HStack {
Image(systemName: "checkmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.green)
Spacer().frame(width: 10)
Text("Green checkmark shield:")
}.font(Font.body.weight(showSecurityHelpAlert == .trusted ? .heavy : .medium))
Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nYou manually verified the used OMEMO keys and no Man-In-The-Middle can take place.")
Spacer().frame(height: 20)
}
}
.onAppear {
//force portrait mode and lock ui there
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
self.appDelegate.obj.orientationLock = .portrait
UIApplication.shared.isIdleTimerDisabled = true
self.ringingPlayer.numberOfLoops = -1
self.busyPlayer.numberOfLoops = -1
self.errorPlayer.numberOfLoops = -1
//ask for mic permissions
AVAudioSession.sharedInstance().requestRecordPermission { granted in
if !granted {
showMicAlert = true
}
}
maybeStartRenderer()
}
.onDisappear {
//allow all orientations again
self.appDelegate.obj.orientationLock = .all
UIApplication.shared.isIdleTimerDisabled = false
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
if MLCallType(rawValue:call.callType) == .video {
call.obj.stopCaptureLocalVideo()
}
}
.onChange(of: MLCallState(rawValue:call.state)) { state in
DDLogVerbose("call state changed: \(String(describing:call.state as NSNumber))")
handleStateChange(call.obj.state, appDelegate.obj.audioState)
}
.onChange(of: MLAudioState(rawValue:appDelegate.audioState)) { audioState in
DDLogVerbose("audioState changed: \(String(describing:appDelegate.audioState as NSNumber))")
handleStateChange(call.obj.state, appDelegate.obj.audioState)
}
}
}
struct AVCallUI_Previews: PreviewProvider {
static var delegate = SheetDismisserProtocol()
static var previews: some View {
AVCallUI(delegate:delegate, call:MLCall.makeDummyCall(0))
}
}

View file

@ -0,0 +1,20 @@
//
// AccountListController.h
// Monal
//
// Created by Anurodh Pokharel on 6/14/13.
//
//
#import <UIKit/UIKit.h>
#import <Monal-Swift.h>
#import "MLSwitchCell.h"
@interface AccountListController : UITableViewController
-(NSUInteger) getAccountNum;
-(NSNumber*) getAccountIDByIndex:(NSUInteger) index;
-(void) setupAccountsView;
-(void) refreshAccountList;
-(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo;
@end

View file

@ -0,0 +1,114 @@
//
// AccountListController.m
// Monal
//
// Created by Anurodh Pokharel on 6/14/13.
//
//
#import "AccountListController.h"
#import "DataLayer.h"
#import "MLXMPPManager.h"
#import "HelperTools.h"
@interface AccountListController ()
@property (nonatomic, strong) NSDateFormatter* uptimeFormatter;
@property (nonatomic, strong) NSIndexPath* selected; // User-selected account - needed for segue
@property (nonatomic, strong) UITableView* accountsTable;
@property (nonatomic, strong) NSArray<NSDictionary*>* accountList;
@end
@implementation AccountListController
#pragma mark View life cycle
- (void) setupAccountsView
{
// Do any additional setup after loading the view.
self.accountsTable.backgroundView = nil;
self.accountsTable = self.tableView;
self.accountsTable.delegate = self;
self.accountsTable.dataSource = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self.accountsTable reloadData];
});
self.uptimeFormatter = [NSDateFormatter new];
self.uptimeFormatter.dateStyle = NSDateFormatterShortStyle;
self.uptimeFormatter.timeStyle = NSDateFormatterShortStyle;
self.uptimeFormatter.doesRelativeDateFormatting = YES;
self.uptimeFormatter.locale = [NSLocale currentLocale];
self.uptimeFormatter.timeZone = [NSTimeZone systemTimeZone];
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(refreshAccountList) name:kMonalAccountStatusChanged object:nil];
}
-(void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(NSUInteger) getAccountNum
{
return self.accountList.count;
}
-(NSNumber*) getAccountIDByIndex:(NSUInteger) index
{
NSNumber* result = [[self.accountList objectAtIndex: index] objectForKey:@"account_id"];
MLAssert(result != nil, @"getAccountIDByIndex, result should not be nil");
return result;
}
-(void) refreshAccountList
{
NSArray* accountList = [[DataLayer sharedInstance] accountList];
dispatch_async(dispatch_get_main_queue(), ^{
self.accountList = accountList;
[self.accountsTable reloadData];
});
}
-(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo
{
[cell initTapCell:@"\n\n"];
NSDictionary* account = [self.accountList objectAtIndex:accNo];
MLAssert(account != nil, ([NSString stringWithFormat:@"Expected non nil account in row %lu", (unsigned long)accNo]));
if([(NSString*)[account objectForKey:@"domain"] length] > 0) {
cell.textLabel.text = [NSString stringWithFormat:@"%@@%@", [[self.accountList objectAtIndex:accNo] objectForKey:@"username"],
[[self.accountList objectAtIndex:accNo] objectForKey:@"domain"]];
}
else
{
cell.textLabel.text = [[self.accountList objectAtIndex:accNo] objectForKey:@"username"];
}
UIImageView* accessory = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
if([[account objectForKey:@"enabled"] boolValue] == YES)
{
cell.imageView.image = [UIImage systemImageNamed:@"checkmark.circle"];
if([[MLXMPPManager sharedInstance] isAccountForIdConnected:[[self.accountList objectAtIndex:accNo] objectForKey:@"account_id"]])
{
accessory.image = [UIImage imageNamed:@"Connected"];
cell.accessoryView = accessory;
}
else
{
accessory.image = [UIImage imageNamed:@"Disconnected"];
cell.accessoryView = accessory;
}
}
else
{
cell.imageView.image = [UIImage systemImageNamed:@"circle"];
accessory.image = nil;
cell.accessoryView = accessory;
}
}
@end

View file

@ -0,0 +1,66 @@
//
// AccountPicker.swift
// Monal
//
// Created by Thilo Molitor on 20.01.23.
// Copyright © 2023 monal-im.org. All rights reserved.
//
struct AccountPicker: View {
let contacts: [MLContact]
let callType: MLCallType
#if IS_ALPHA
let appLogoId = "AlphaAppLogo"
#elseif IS_QUICKSY
let appLogoId = "QuicksyAppLogo"
#else
let appLogoId = "AppLogo"
#endif
init(contacts:[MLContact], callType: MLCallType) {
self.contacts = contacts
self.callType = callType
}
var body: some View {
//ScrollView {
VStack {
HStack () {
Image(decorative: appLogoId)
.resizable()
.frame(width: CGFloat(120), height: CGFloat(120), alignment: .center)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.padding()
Text("You are trying to call '\(contacts.first!.contactDisplayName)' (\(contacts.first!.contactJid)), but this contact can be reached using different accounts. Please select the account you want to place the outgoing call with.")
.padding()
.padding(.leading, -16.0)
}
.frame(maxWidth: .infinity)
.background(Color(UIColor.systemBackground))
List {
ForEach(contacts) { contact in
if let accountEntry = DataLayer.sharedInstance().details(forAccount:contact.accountID) {
let accountJid = "\(accountEntry["username"] ?? "<unknown>" as NSString)@\(accountEntry["domain"] ?? "<unknown>" as NSString)"
let accountContact = MLContact.createContact(fromJid:accountJid, andAccountID:accountEntry["account_id"] as! NSNumber)
Button {
(UIApplication.shared.delegate as! MonalAppDelegate).activeChats!.call(contact, with:callType)
} label: {
ContactEntry(contact:ObservableKVOWrapper(accountContact), selfnotesPrefix:false)
}
}
}
}
.listStyle(.insetGrouped)
}
//}
.textFieldStyle(.roundedBorder)
.navigationBarTitle(Text("Account Picker"))
}
}
struct AccountPicker_Previews: PreviewProvider {
static var previews: some View {
AccountPicker(contacts:[MLContact.makeDummyContact(0)], callType:.audio)
}
}

View file

@ -0,0 +1,64 @@
//
// ActiveChatsViewController.h
// Monal
//
// Created by Anurodh Pokharel on 6/14/13.
//
//
#import <UIKit/UIKit.h>
#import "MLConstants.h"
#import "MLContact.h"
#import "MLCall.h"
#import <DZNEmptyDataSet/UIScrollView+EmptyDataSet.h>
NS_ASSUME_NONNULL_BEGIN
@class UIHostingControllerWorkaround;
@class chatViewController;
@class MLCall;
@interface SizeClassWrapper: NSObject
@property (atomic) UIUserInterfaceSizeClass horizontal;
@end
@interface ActiveChatsViewController : UITableViewController <DZNEmptyDataSetSource, DZNEmptyDataSetDelegate>
@property (nonatomic, strong) UITableView* chatListTable;
@property (nonatomic, weak) IBOutlet UIBarButtonItem* settingsButton;
@property (weak, nonatomic) IBOutlet UIBarButtonItem* spinnerButton;
@property (nonatomic, weak) IBOutlet UIBarButtonItem* composeButton;
@property (nonatomic, strong) UIActivityIndicatorView* spinner;
@property (atomic, strong) SizeClassWrapper* sizeClass;
@property (atomic, readonly) chatViewController* _Nullable currentChatView;
-(void) showCallContactNotFoundAlert:(NSString*) jid;
-(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender;
-(void) callContact:(MLContact*) contact withCallType:(MLCallType) callType;
-(void) presentAccountPickerForContacts:(NSArray<MLContact*>*) contacts andCallType:(MLCallType) callType;
-(void) presentCall:(MLCall*) call;
-(void) presentChatWithContact:(MLContact* _Nullable) contact;
-(void) presentChatWithContact:(MLContact* _Nullable) contact andCompletion:(monal_id_block_t _Nullable) completion;
-(void) presentSplitPlaceholder;
-(void) refreshDisplay;
-(void) showContacts;
-(void) deleteConversation;
-(void) showSettings;
-(void) showGeneralSettings;
-(void) prependGeneralSettings;
-(void) showNotificationSettings;
-(void) showDetails;
-(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString* _Nullable) token usingCompletion:(monal_id_block_t _Nullable) callback;
-(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) preauthToken prefillAccount:(xmpp* _Nullable) account andOmemoFingerprints:(NSDictionary* _Nullable) fingerprints;
-(void) showAddContact;
-(void) sheetDismissed;
-(void) segueToIntroScreensIfNeeded;
-(void) resetViewQueue;
-(void) dismissCompleteViewChainWithAnimation:(BOOL) animation andCompletion:(monal_void_block_t _Nullable) completion;
-(void) updateSizeClass;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,345 @@
//
// AddContactMenu.swift
// Monal
//
// Created by Jan on 27.10.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
import MobileCoreServices
import UniformTypeIdentifiers
struct AddContactMenu: View {
var delegate: SheetDismisserProtocol
private static let jidFaultyPattern = "^([^@]+@)?.+(\\..{2,})?$"
@State private var enabledAccounts: [xmpp]
@State private var selectedAccount: Int
@State private var scannedFingerprints: [NSNumber: Data]? = nil
@State private var importScannedFingerprints: Bool = false
@State private var toAdd: String = ""
@State private var showInvitationError = false
@State private var showAlert = false
// note: dismissLabel is not accessed but defined at the .alert() section
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
@State private var invitationResult: [String: AnyObject]? = nil
@StateObject private var overlay = LoadingOverlayState()
@State private var showQRCodeScanner = false
@State private var success = false
@State private var newContact: MLContact?
@State private var isEditingJid = false
private let dismissWithNewContact: (MLContact) -> Void
private let preauthToken: String?
init(delegate: SheetDismisserProtocol, dismissWithNewContact: @escaping (MLContact) -> Void, prefillJid: String = "", preauthToken: String? = nil, prefillAccount: xmpp? = nil, omemoFingerprints: [NSNumber: Data]? = nil) {
self.delegate = delegate
self.dismissWithNewContact = dismissWithNewContact
// self.toAdd = State(wrappedValue: prefillJid)
toAdd = prefillJid
self.preauthToken = preauthToken
// only display omemo ui part if there are any fingerprints (the checks below test for nil, not for 0)
if omemoFingerprints?.count ?? 0 > 0 {
scannedFingerprints = omemoFingerprints
}
let enabledAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp]
self.enabledAccounts = enabledAccounts
selectedAccount = enabledAccounts.first != nil ? 0 : -1
if let prefillAccount = prefillAccount {
for index in enabledAccounts.indices {
if enabledAccounts[index].accountID.isEqual(to: prefillAccount.accountID) {
selectedAccount = index
}
}
}
}
// FIXME: duplicate code from WelcomeLogIn.swift, maybe move to SwiftuiHelpers
private var toAddEmptyAlert: Bool {
alertPrompt.title = Text("No Empty Values!")
alertPrompt.message = Text("Please make sure you have entered a valid jid.")
return toAddEmpty
}
private var toAddInvalidAlert: Bool {
alertPrompt.title = Text("Invalid Credentials!")
alertPrompt.message = Text("The jid you want to add should be in in the format user@domain.tld.")
return toAddInvalid
}
private func errorAlert(title: Text, message: Text = Text("")) {
alertPrompt.title = title
alertPrompt.message = message
showAlert = true
}
private func successAlert(title: Text, message: Text) {
alertPrompt.title = title
alertPrompt.message = message
success = true // < dismiss entire view on close
showAlert = true
}
private var toAddEmpty: Bool {
toAdd.isEmpty
}
private var toAddInvalid: Bool {
toAdd.range(of: AddContactMenu.jidFaultyPattern, options: .regularExpression) == nil
}
func trustFingerprints(_ fingerprints: [NSNumber: Data]?, for jid: String, on account: xmpp) {
// we don't untrust other devices not included in here, because conversations only exports its own fingerprint
if let fingerprints = fingerprints {
for (deviceId, fingerprint) in fingerprints {
let address = SignalAddress(name: jid, deviceId: deviceId.int32Value)
let knownDevices = Array(account.omemo.knownDevices(forAddressName: jid))
if !knownDevices.contains(deviceId) {
account.omemo.addIdentityManually(address, identityKey: fingerprint)
assert(account.omemo.getIdentityFor(address) == fingerprint, "The stored and created fingerprint should match")
}
// trust device/fingerprint if fingerprints match
let knownFingerprintHex = HelperTools.signalHexKey(with: account.omemo.getIdentityFor(address))
let addedFingerprintHex = HelperTools.signalHexKey(with: fingerprint)
if knownFingerprintHex.uppercased() == addedFingerprintHex.uppercased() {
account.omemo.updateTrust(true, for: address)
}
}
}
}
func addJid(jid: String) {
let account = enabledAccounts[selectedAccount]
let contact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
if contact.isInRoster {
newContact = contact
// import omemo fingerprints as manually trusted, if requested
trustFingerprints(importScannedFingerprints ? scannedFingerprints : [:], for: jid, on: account)
// only alert of already known contact if we did not import the omemo fingerprints
if !importScannedFingerprints || scannedFingerprints?.count ?? 0 == 0 {
if enabledAccounts.count > 1 {
success = true
successAlert(title: Text("Already present"), message: Text("This contact is already in the contact list of the selected account"))
} else {
success = true
successAlert(title: Text("Already present"), message: Text("This contact is already in your contact list"))
}
}
return
}
showPromisingLoadingOverlay(overlay, headline: NSLocalizedString("Adding...", comment: ""), description: "") {
account.checkJidType(jid)
}.done { type in
let type = type as! String
if type == "account" {
let contact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
self.newContact = contact
MLXMPPManager.sharedInstance().add(contact, withPreauthToken: preauthToken)
// import omemo fingerprints as manually trusted, if requested
trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for: jid, on: account)
successAlert(title: Text("Permission Requested"), message: Text("The new contact will be added to your contacts list when the person you've added has approved your request."))
} else if type == "muc" {
showPromisingLoadingOverlay(overlay, headlineView: Text("Adding Group/Channel..."), descriptionView: Text("")) {
promisifyMucAction(account: account, mucJid: jid) {
account.joinMuc(jid)
}
}.done { _ in
self.newContact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
successAlert(title: Text("Success!"), message: Text("Successfully joined group/channel \(jid)!"))
}.catch { error in
errorAlert(title: Text("Error entering group/channel!"), message: Text("\(String(describing: error))"))
}
}
}.catch { error in
errorAlert(title: Text("Error"), message: Text(error.localizedDescription))
}
}
var body: some View {
let account = enabledAccounts[selectedAccount]
let splitJid = HelperTools.splitJid(account.connectionProperties.identity.jid)
Form {
if enabledAccounts.isEmpty {
Text("Please make sure at least one account has connected before trying to add a contact or channel.")
.foregroundColor(.secondary)
} else {
if DataLayer.sharedInstance().allContactRequests().count > 0 {
ContactRequestsMenu()
}
Section(header: Text("Contact and Group/Channel Jids are usually in the format: name@domain.tld")) {
if enabledAccounts.count > 1 {
Picker("Use account", selection: $selectedAccount) {
ForEach(Array(self.enabledAccounts.enumerated()), id: \.element) { idx, account in
Text(account.connectionProperties.identity.jid).tag(idx)
}
}
.pickerStyle(.menu)
}
TextField(NSLocalizedString("Contact-, Group- or Channel-Jid", comment: "placeholder when adding jid"), text: $toAdd, onEditingChanged: { isEditingJid = $0 })
.textInputAutocapitalization(.never)
.autocapitalization(.none)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.addClearButton(isEditing: isEditingJid, text: $toAdd)
.disabled(scannedFingerprints != nil)
.foregroundColor(scannedFingerprints != nil ? .secondary : .primary)
.onChange(of: toAdd) { _ in toAdd = toAdd.replacingOccurrences(of: " ", with: "") }
if scannedFingerprints != nil && !scannedFingerprints!.isEmpty {
Section(header: Text("A contact was scanned through the QR code scanner")) {
Toggle(isOn: $importScannedFingerprints) {
Text("Import and trust OMEMO fingerprints from QR code")
}
}
}
if scannedFingerprints != nil {
Button(action: {
toAdd = ""
importScannedFingerprints = true
scannedFingerprints = nil
}, label: {
Text("Clear scanned contact")
.foregroundColor(.red)
})
}
HStack {
Spacer()
Button(action: {
showAlert = toAddEmptyAlert || toAddInvalidAlert
if !showAlert {
let jidComponents = HelperTools.splitJid(toAdd)
if jidComponents["host"] == nil || jidComponents["host"]!.isEmpty {
errorAlert(title: Text("Error"), message: Text("Something went wrong while parsing your input..."))
showAlert = true
return
}
// use the canonized jid from now on (lowercased, resource removed etc.)
addJid(jid: jidComponents["user"]!)
}
}) {
scannedFingerprints == nil ? Text("Add") : Text("Add scanned contact")
}
.disabled(toAddEmpty || toAddInvalid)
.buttonStyle(MonalProminentButtonStyle())
}
}
if DataLayer.sharedInstance().allContactRequests().count == 0 {
Section {
ContactRequestsMenu()
}
}
}
}
.padding()
.alert(isPresented: $showAlert) {
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(Text("Close"), action: {
showAlert = false
if self.success == true {
if self.newContact != nil {
self.dismissWithNewContact(newContact!)
} else {
self.delegate.dismiss()
}
}
}))
}
.richAlert(isPresented: $invitationResult, title: Text("Invitation for \(splitJid["host"]!) created")) { data in
VStack {
Image(uiImage: createQrCode(value: data["landing"] as! String))
.interpolation(.none)
.resizable()
.scaledToFit()
.aspectRatio(1, contentMode: .fit)
if let expires = data["expires"] as? Date {
Text("This invitation will expire on \(expires.formatted(date: .numeric, time: .shortened))")
.font(.footnote)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
} buttons: { data in
Button(action: {
UIPasteboard.general.setValue(data["landing"] as! String, forPasteboardType: UTType.utf8PlainText.identifier as String)
invitationResult = nil
}) {
ShareLink("Share invitation link", item: URL(string: data["landing"] as! String)!)
}
Button(action: {
invitationResult = nil
}) {
Text("Close")
.frame(maxWidth: .infinity)
}
}
.sheet(isPresented: $showQRCodeScanner) {
NavigationStack {
MLQRCodeScanner(handleClose: {
self.showQRCodeScanner = false
})
.navigationTitle("QR-Code Scanner")
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
self.showQRCodeScanner = false
}, label: {
Text("Close")
})
})
})
}
}
.navigationBarTitle(Text("Add Contact or Channel"), displayMode: .inline)
.toolbar(content: {
ToolbarItemGroup(placement: .navigationBarTrailing) {
if account.connectionProperties.discoveredAdhocCommands["urn:xmpp:invite#invite"] != nil {
Button(action: {
DDLogVerbose("Trying to create invitation for: \(String(describing: splitJid["host"]!))")
showLoadingOverlay(overlay, headline: NSLocalizedString("Creating invitation...", comment: ""))
account.createInvitation(completion: {
let result = $0 as! [String: AnyObject]
DispatchQueue.main.async {
hideLoadingOverlay(overlay)
DDLogVerbose("Got invitation result: \(String(describing: result))")
if result["success"] as! Bool == true {
invitationResult = result
} else {
errorAlert(title: Text("Failed to create invitation for \(splitJid["host"]!)"), message: Text(result["error"] as! String))
}
}
})
}, label: {
Image(systemName: "square.and.arrow.up")
})
}
Button(action: {
self.showQRCodeScanner = true
}, label: {
Image(systemName: "camera.fill")
})
}
})
.addLoadingOverlay(overlay)
}
}
struct AddContactMenu_Previews: PreviewProvider {
static var delegate = SheetDismisserProtocol()
static var previews: some View {
AddContactMenu(delegate: delegate, dismissWithNewContact: { _ in
})
}
}

View file

@ -0,0 +1,100 @@
//
// BackgroundSettings.swift
// Monal
//
// Created by Thilo Molitor on 14.11.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
//swiftui is somehow needed to let the PhotosUI import succeed, even if it's already imported by SwiftuiHelpers.swift using @_exported
import SwiftUI
import PhotosUI
@ViewBuilder
func title(contact: ObservableKVOWrapper<MLContact>?) -> some View {
if let contact = contact {
Text("Select a background to display behind conversations with \(contact.contactDisplayName as String)")
} else {
Text("Select a default background to display behind conversations.")
}
}
struct BackgroundSettings: View {
@State private var selectedItem: PhotosPickerItem? = nil
@State private var showingImagePicker = false
@State private var inputImage: UIImage?
let contact: ObservableKVOWrapper<MLContact>?
init(contact: ObservableKVOWrapper<MLContact>?) {
self.contact = contact
_inputImage = State(initialValue:MLImageManager.sharedInstance().getBackgroundFor(self.contact?.obj))
}
var body: some View {
VStack {
Form {
Section(header:title(contact:contact)) {
VStack(spacing: 20) {
Spacer().frame(height: 0)
PhotosPicker(selection:$selectedItem, matching:.images, photoLibrary:.shared()) {
if let inputImage = inputImage {
HStack(alignment: .center) {
Image(uiImage:inputImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity, alignment: .center)
}
.addTopRight {
Button(action: {
self.inputImage = nil
}, label: {
Image(systemName: "xmark.circle.fill")
.resizable()
.frame(width: 32.0, height: 32.0)
.accessibilityLabel(Text("Remove Background Image"))
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
})
.buttonStyle(.borderless)
.offset(x: 12, y: -12)
}
} else {
Text("Select background image")
.frame(maxWidth: .infinity, alignment: .center)
}
}
.accessibilityLabel(Text("Change Background Image"))
.onChange(of:selectedItem) { newItem in
// Retrive selected asset in the form of Data
newItem?.loadTransferable(type:Data.self) { result in
guard let data = try? result.get() else {
self.inputImage = nil
return
}
guard let loadedImage = UIImage(data: data) else {
self.inputImage = nil
return
}
self.inputImage = loadedImage
}
}
Spacer().frame(height: 0)
}
}
}
}
.navigationBarTitle(contact != nil ? Text("Chat Background") : Text("Default Background"))
.onChange(of:inputImage) { _ in
MLImageManager.sharedInstance().saveBackgroundImageData(inputImage?.pngData(), for:self.contact?.obj)
}
}
}
struct BackgroundSettings_Previews: PreviewProvider {
static var previews: some View {
BackgroundSettings(contact:nil)
}
}

View file

@ -0,0 +1,119 @@
//
// BlockedUsers.swift
// Monal
//
// Created by lissine on 10/9/2024.
// Copyright © 2024 monal-im.org. All rights reserved.
//
struct BlockedUsers: View {
let xmppAccount: xmpp
static private let jidPattern = "^([^@]+@)?[^/\\n]+(\\..{2,})?(/.+)?$"
@State private var blockedJids: [String] = []
@State private var jidToBlock = ""
@State private var showAddingToBlocklistForm = false
@State private var showBlockingUnsupportedPlaceholder = false
@State private var showInvalidJidAlert = false
@StateObject private var overlay = LoadingOverlayState()
private var blockingUnsupported: Bool {
return !xmppAccount.connectionProperties.serverDiscoFeatures.contains("urn:xmpp:blocking")
}
private func reloadBlocksFromDB() {
self.blockedJids = DataLayer.sharedInstance().blockedJids(forAccount: xmppAccount.accountID)
}
var body: some View {
if showBlockingUnsupportedPlaceholder {
ContentUnavailableShimView("Blocking unsupported", systemImage: "iphone.homebutton.slash", description: Text("Your server does not support blocking (XEP-0191)."))
} else {
List {
ForEach(blockedJids, id: \.self) { blockedJid in
Text(blockedJid)
}
.onDelete { indexSet in
for row in indexSet {
showLoadingOverlay(overlay, headlineView: Text("Saving changes to server"), descriptionView: Text(""))
// unblock the jid
MLXMPPManager.sharedInstance().block(false, fullJid: self.blockedJids[row], onAccount: self.xmppAccount.accountID)
}
}
}
.listStyle(.plain)
.navigationTitle("Blocked Users")
.animation(.default, value: blockedJids)
.onAppear {
if !(xmppAccount.accountState.rawValue >= xmppState.stateBound.rawValue && xmppAccount.connectionProperties.accountDiscoDone) {
showLoadingOverlay(overlay, headlineView: Text("Account is connecting..."), descriptionView: Text(""))
}
showBlockingUnsupportedPlaceholder = blockingUnsupported
reloadBlocksFromDB()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalAccountDiscoDone")).receive(on: RunLoop.main)) { notification in
guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber,
notificationAccountID.intValue == xmppAccount.accountID.intValue else {
return
}
// recompute this state variable, so the view is re-rendered if it changed.
showBlockingUnsupportedPlaceholder = blockingUnsupported
reloadBlocksFromDB()
hideLoadingOverlay(overlay)
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalBlockListRefresh")).receive(on: RunLoop.main)) { notification in
guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber,
notificationAccountID.intValue == xmppAccount.accountID.intValue else {
return
}
DispatchQueue.main.async {
reloadBlocksFromDB()
DDLogVerbose("Got block list update from account \(xmppAccount)...")
hideLoadingOverlay(overlay)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAddingToBlocklistForm = true
}, label: {
Image(systemName: "plus")
})
}
}
.alert("Enter the jid that you want to block", isPresented: $showAddingToBlocklistForm, actions: {
TextField("user@example.org/resource", text: $jidToBlock)
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.autocorrectionDisabled()
Button("Block", role: .destructive) {
guard (jidToBlock.range(of: BlockedUsers.jidPattern, options: .regularExpression) != nil) else {
showInvalidJidAlert = true
return
}
showLoadingOverlay(overlay, headlineView: Text("Saving changes to server"), descriptionView: Text(""))
// block the jid
MLXMPPManager.sharedInstance().block(true, fullJid: jidToBlock, onAccount: self.xmppAccount.accountID)
}
Button("Cancel", role: .cancel, action: {})
}
)
// If .onDisappear is applied to the alert or any of its subviews, its perform action won't
// get executed until the whole Blocked Users view is dismissed. Therefore .onChange is used instead
.onChange(of: showAddingToBlocklistForm) { _ in
if !showAddingToBlocklistForm {
// The alert has been dismissed
jidToBlock = ""
}
}
.alert("Input is not a valid jid", isPresented: $showInvalidJidAlert, actions: {})
.addLoadingOverlay(overlay)
}
}
}

View file

@ -0,0 +1,254 @@
//
// BoardingCards.swift
// Monal
//
// Created by Vaidik Dubey on 05/06/24.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import FrameUp
class OnboardingState: ObservableObject {
@defaultsDB("hasCompletedOnboarding")
var hasCompletedOnboarding: Bool
}
struct OnboardingCard: Identifiable {
let id = UUID()
let title: Text?
let description: Text?
let imageName: String?
let articleText: Text?
let customView: AnyView?
let nextText: String?
}
struct OnboardingView: View {
var delegate: SheetDismisserProtocol
let cards: [OnboardingCard]
@ObservedObject var onboardingState = OnboardingState()
@State private var currentIndex = 0
var body: some View {
ZStack {
/// Ensure the ZStack takes the entire area
Color.clear
ForEach(Array(zip(cards, cards.indices)), id: \.1) { card, index in
/// Only show card that's visible
if index == currentIndex {
GeometryReader { proxy in
SmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: false) {
VStack(alignment: .leading, spacing: 16) {
if currentIndex > 0 {
Button {
currentIndex -= 1
} label: {
Label("Back", systemImage: "chevron.left")
.labelStyle(.iconOnly)
.padding(10)
}
} else {
//make sure the space the "back" label will take, is already reserved to not have "jumps" when pressing next
Text("").padding(10)
}
HStack {
if let imageName = card.imageName {
Image(systemName: imageName)
.font(.custom("MarkerFelt-Wide", size: 80))
.foregroundColor(.accentColor)
.accessibilityHidden(true)
}
card.title?
.font(.title)
.fontWeight(.bold)
.foregroundColor(.primary)
.padding(.bottom, 4)
/// This ensures text doesn't get truncated which sometimes happens in ScrollView
.fixedSize(horizontal: false, vertical: true)
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isHeader)
if let description = card.description {
description
.font(.custom("HelveticaNeue-Medium", size: 20))
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
/// This ensures text doesn't get truncated which sometimes happens in ScrollView
.fixedSize(horizontal: false, vertical: true)
}
if card.imageName != nil || card.description != nil || card.imageName != nil {
Spacer().frame(height: 1)
Divider()
Spacer().frame(height: 1)
}
card.articleText?
.font(.custom("HelveticaNeue-Medium", size: 20))
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
card.customView
Spacer()
Group {
if index < cards.count - 1 {
Button {
currentIndex += 1
} label: {
HStack {
Text(card.nextText ?? NSLocalizedString("Next", comment:"onboarding"))
.fontWeight(.bold)
Image(systemName: "chevron.right")
}
}
} else {
Button {
onboardingState.hasCompletedOnboarding = true
delegate.dismissWithoutAnimation()
} label: {
Text(card.nextText ?? NSLocalizedString("Close", comment:"onboarding"))
}
.buttonStyle(MonalProminentButtonStyle())
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.bottom, 16)
.padding()
/// Sets the minimum frame height to the available height of the scrollview and the maxHeight to infinity
.frame(minHeight: proxy.size.height, maxHeight: .infinity)
}
}
.accessibilityAddTraits(.isModal)
}
}
}
.onAppear {
if UIDevice.current.userInterfaceIdiom != .pad {
//force portrait mode and lock ui there
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
(UIApplication.shared.delegate as! MonalAppDelegate).orientationLock = .portrait
}
}
}
}
@ViewBuilder
func createOnboardingView(delegate: SheetDismisserProtocol) -> some View {
#if IS_QUICKSY
let cards = [
OnboardingCard(
title: Text("Welcome to Quicksy !"),
description: nil,
imageName: "hand.wave",
articleText: Text("""
Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts who are already on Quicksy.
Quicksy shares and stores images, audio recordings, videos and other media to deliver them to the intended recipients. Files will be stored for up to 30 days.
Find more Information in our [Privacy Policy](https://quicksy.im/privacy.htm).
"""),
customView: nil,
nextText: "Accept and continue"
),
]
#else
let cards = [
OnboardingCard(
title: Text("Welcome to Monal !"),
description: Text("Become part of a worldwide decentralized chat network!"),
imageName: "hand.wave",
articleText: Text("""
Modern iOS and macOS XMPP chat client.\n\nXMPP is a federated network: Just like email, you can register your account on many servers and still talk to anyone, even if they signed up on a different server.\n\nUsing Monal instead of a centralized chat app therefore increases your digital sovereignty.
"""),
customView: nil,
nextText: nil
),
OnboardingCard(
title: Text("Features"),
description: nil,
imageName: "sparkles",
articleText: Text("""
🛜 Decentralized Network :
Leverages the decentralized nature of XMPP, avoiding central servers and increasing your digital sovereignty.
🌐 Data privacy :
We do not sell or track information for external parties (nor for anyone else).
🔐 End-to-end encryption :
Secure multi-end messaging using the OMEMO protocol.
👨💻 Open Source :
The app's source code is publicly available for audit and contribution.
"""),
customView: nil,
nextText: nil
),
OnboardingCard(
title: Text("Settings"),
description: Text("These are important privacy settings you may want to review!"),
imageName: "gear",
articleText: nil,
customView: AnyView(PrivacySettingsSubview(onboardingPart:0)),
nextText: nil
),
OnboardingCard(
title: Text("Settings"),
description: Text("These are important privacy settings you may want to review!"),
imageName: "gear",
articleText: nil,
customView: AnyView(PrivacySettingsSubview(onboardingPart:1)),
nextText: nil
),
OnboardingCard(
title: Text("Even more to customize!"),
description: Text("You can customize even more, just use the button below to open the settings."),
imageName: "hand.wave",
articleText: nil,
customView: AnyView(TakeMeToSettingsView(delegate:delegate)),
nextText: nil
),
]
#endif
OnboardingView(delegate: delegate, cards: cards)
}
struct TakeMeToSettingsView: View {
@ObservedObject var onboardingState = OnboardingState()
var delegate: SheetDismisserProtocol
var body: some View {
HStack {
Spacer()
Button(action: {
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
if let activeChats = appDelegate.activeChats {
activeChats.prependGeneralSettings()
}
onboardingState.hasCompletedOnboarding = true
delegate.dismissWithoutAnimation()
}) {
Text("Take me to settings")
}
.buttonStyle(MonalProminentButtonStyle())
Spacer()
}
}
}
struct OnboardingView_Previews: PreviewProvider {
static var delegate = SheetDismisserProtocol()
static var previews: some View {
createOnboardingView(delegate: delegate)
.environmentObject(OnboardingState())
}
}

View file

@ -0,0 +1,77 @@
//
// ChannelMemberList.swift
// Monal
//
// Created by Friedrich Altheide on 17.02.24.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import OrderedCollections
struct ChannelMemberList: View {
private let account: xmpp
@State private var ownAffiliation: String;
@StateObject var channel: ObservableKVOWrapper<MLContact>
@State private var participants: OrderedDictionary<String, String>
init(mucContact: ObservableKVOWrapper<MLContact>) {
account = mucContact.obj.account! as xmpp
_channel = StateObject(wrappedValue:mucContact)
_ownAffiliation = State(wrappedValue:kMucAffiliationNone)
_participants = State(wrappedValue:OrderedDictionary<String, String>())
}
func updateParticipantList() {
ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:channel.obj) ?? kMucAffiliationNone
participants.removeAll(keepingCapacity:true)
for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:channel.contactJid, forAccountID:account.accountID)) {
//ignore ourselves
if let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String {
if jid == account.connectionProperties.identity.jid {
continue
}
}
if let nick = memberInfo["room_nick"] as? String {
participants[nick] = memberInfo["affiliation"] as? String ?? kMucAffiliationNone
}
}
participants.sort {
(mucAffiliationToInt($0.value), $0.key.lowercased()) < (mucAffiliationToInt($1.value), $1.key.lowercased())
}
}
var body: some View {
List {
Section(header: Text("\(self.channel.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) {
ForEach(participants.keys, id: \.self) { participant_key in
ZStack(alignment: .topLeading) {
HStack(alignment: .center) {
Text(participant_key)
Spacer()
Text(mucAffiliationToString(participants[participant_key]))
}
}
}
}
}
.navigationBarTitle(Text("Channel Participants"), displayMode: .inline)
.onAppear {
updateParticipantList()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact {
DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...")
if contact == channel {
updateParticipantList()
}
}
}
}
}
struct ChannelMemberList_Previews: PreviewProvider {
static var previews: some View {
ChannelMemberList(mucContact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(3)));
}
}

View file

@ -0,0 +1,29 @@
//
// ChatPlaceholder.swift
// Monal
//
// Created by Thilo Molitor on 30.11.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
struct ChatPlaceholder: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
ZStack {
if colorScheme == .dark {
Color.black
} else {
Color.white
}
Image(colorScheme == .dark ? "park_white_black" : "park_colors")
.resizable()
.scaledToFill()
}
}
}
struct ChatPlaceholder_Previews: PreviewProvider {
static var previews: some View {
ChatPlaceholder()
}
}

View file

@ -0,0 +1,701 @@
//
// ContactDetails.swift
// Monal
//
// Created by Jan on 22.10.21.
// Copyright © 2021 Monal.im. All rights reserved.
//
struct ContactDetails: View {
@Environment(\.presentationMode) private var presentationMode
@State private var ownRole = kMucRoleParticipant
@State private var ownAffiliation = kMucAffiliationNone
@StateObject var contact: ObservableKVOWrapper<MLContact>
@State private var showingRemoveAvatarConfirmation = false
@State private var showingBlockContactConfirmation = false
@State private var showingCannotBlockAlert = false
@State private var showingRemoveContactConfirmation = false
@State private var showingAddContactConfirmation = false
@State private var showingClearHistoryConfirmation = false
@State private var showingResetOmemoSessionConfirmation = false
@State private var showingCannotEncryptAlert = false
@State private var showingShouldDisableEncryptionAlert = false
@State private var isEditingNickname = false
@State private var inputImage: UIImage?
@State private var showingImagePicker = false
@State private var showingSheetEditSubject = false
@State private var showingDestroyConfirmation = false
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
@State private var showAlert = false
@State private var success = false
@State private var successCallback: monal_void_block_t?
@StateObject private var overlay = LoadingOverlayState()
var delegate: SheetDismisserProtocol?
private var account: xmpp
init(delegate: SheetDismisserProtocol?, contact: ObservableKVOWrapper<MLContact>) {
self.delegate = delegate
_contact = StateObject(wrappedValue: contact)
self.account = contact.obj.account!
}
private func updateRoleAndAffiliation() {
if contact.isMuc {
self.ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? kMucRoleNone
self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? kMucAffiliationNone
} else {
self.ownRole = kMucRoleParticipant
self.ownAffiliation = kMucAffiliationNone
}
}
private func errorAlert(title: Text, message: Text = Text("")) {
alertPrompt.title = title
alertPrompt.message = message
showAlert = true
}
private func successAlert(title: Text, message: Text = Text("")) {
alertPrompt.title = title
alertPrompt.message = message
showAlert = true
success = true // < dismiss entire view on close
}
private func showImagePicker() {
#if targetEnvironment(macCatalyst)
let picker = DocumentPickerViewController(
supportedTypes: [UTType.image],
onPick: { url in
if let imageData = try? Data(contentsOf: url) {
if let loadedImage = UIImage(data: imageData) {
self.inputImage = loadedImage
}
}
},
onDismiss: {
//do nothing on dismiss
}
)
UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true)
#else
showingImagePicker = true
#endif
}
var body: some View {
Form {
Section {
VStack(spacing: 20) {
if !contact.isSelfChat {
Image(uiImage: contact.avatar)
.resizable()
.scaledToFit()
.applyClosure {view in
if contact.isMuc {
if ownAffiliation == kMucAffiliationOwner {
view.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Change Group Avatar") : Text("Change Channel Avatar"))
.onTapGesture {
showImagePicker()
}
.addTopRight {
if contact.hasAvatar {
Button(action: {
showingRemoveAvatarConfirmation = true
}, label: {
Image(systemName: "xmark.circle.fill")
.resizable()
.frame(width: 24.0, height: 24.0)
.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Remove Group Avatar") : Text("Remove Channel Avatar"))
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
})
.buttonStyle(.borderless)
.offset(x: 8, y: -8)
} else {
Button(action: {
showImagePicker()
}, label: {
Image(systemName: "pencil.circle.fill")
.resizable()
.frame(width: 24.0, height: 24.0)
.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Change Group Avatar") : Text("Change Channel Avatar"))
})
.buttonStyle(.borderless)
.offset(x: 8, y: -8)
}
}
} else {
view.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Group Avatar") : Text("Channel Avatar"))
}
} else {
view.accessibilityLabel(Text("Avatar"))
}
}
.frame(width: 150, height: 150, alignment: .center)
.shadow(radius: 7)
.actionSheet(isPresented: $showingRemoveAvatarConfirmation) {
ActionSheet(
title: Text("Really remove avatar?"),
message: Text("This will remove the current avatar image and revert this group/channel to the default one."),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
showPromisingLoadingOverlay(overlay, headlineView:Text("Removing avatar..."), descriptionView:Text("")) {
promisifyMucAction(account:account, mucJid:contact.contactJid) {
self.account.mucProcessor.publishAvatar(nil, forMuc: contact.contactJid)
}
}.catch { error in
errorAlert(title: Text("Error removing avatar!"), message: Text("\(String(describing:error))"))
hideLoadingOverlay(overlay)
}
}
)
]
)
}
}
Button {
UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String)
UIAccessibility.post(notification: .announcement, argument: "JID Copied")
} label: {
HStack {
Text(contact.contactJid as String)
Image(systemName: "doc.on.doc")
.foregroundColor(.primary)
.accessibilityHidden(true)
}
.accessibilityHint("Copies JID")
}
.buttonStyle(.borderless)
// //TODO: wait for account edit to become swiftui
// if contact.isSelfChat {
// Button {
// //TODO: open account edit
// } label: {
// Text("Open account settings")
// .accessibilityHint("Open account settings")
// }
// .buttonStyle(.borderless)
// }
//only show account jid if more than one is configured
if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat {
Text("Account: \(account.connectionProperties.identity.jid)")
}
if !contact.isSelfChat && !contact.isMuc {
if let lastInteractionTime = contact.lastInteractionTime as Date? {
if lastInteractionTime.timeIntervalSince1970 > 0 {
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""),
DateFormatter.localizedString(from: lastInteractionTime, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short)))
} else {
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("now", comment: "")))
}
} else {
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("unknown", comment: "")))
}
}
if !contact.isMuc, let statusMessage = contact.statusMessage as String?, statusMessage.count > 0 {
VStack {
Text("Status message:")
Text(contact.statusMessage as String)
.fixedSize(horizontal: false, vertical: true)
}
}
if contact.isMuc && ((contact.groupSubject as String).count > 0 || ownRole == kMucRoleModerator) {
VStack {
if ownRole == kMucRoleModerator {
Button {
showingSheetEditSubject.toggle()
} label: {
if contact.obj.mucType == kMucTypeGroup {
HStack {
Text("Group subject:")
Spacer().frame(width:8)
Image(systemName: "pencil")
.foregroundColor(.primary)
.accessibilityHidden(true)
}
.accessibilityHint("Edit Group Subject")
} else {
HStack {
Text("Channel subject:")
Spacer().frame(width:8)
Image(systemName: "pencil")
.foregroundColor(.primary)
.accessibilityHidden(true)
}
.accessibilityHint("Edit Channel Subject")
}
}
.buttonStyle(.borderless)
} else {
Text("Group subject:")
}
Text(contact.groupSubject as String)
.fixedSize(horizontal: false, vertical: true)
}
}
}
.foregroundColor(.primary)
.padding([.top, .bottom])
.frame(maxWidth: .infinity)
}
// info/nondestructive buttons
Section {
if !contact.isSelfChat {
Button {
if contact.isMuc {
if !contact.isMuted && !contact.isMentionOnly {
contact.obj.toggleMentionOnly(true)
} else if !contact.isMuted && contact.isMentionOnly {
contact.obj.toggleMentionOnly(false)
contact.obj.toggleMute(true)
} else {
contact.obj.toggleMentionOnly(false)
contact.obj.toggleMute(false)
}
} else {
contact.obj.toggleMute(!contact.isMuted)
}
} label: {
if contact.isMuted {
Label {
contact.isMuc ? Text("Notifications disabled") : Text("Contact is muted")
} icon: {
Image(systemName: "bell.slash.fill")
}
.foregroundStyle(Color.red)
} else if contact.isMuc && contact.isMentionOnly {
Label {
Text("Notify only when mentioned")
} icon: {
Image(systemName: "bell.badge")
}
.foregroundStyle(Color.primary)
} else {
Label {
contact.isMuc ? Text("Notify on all messages") : Text("Contact is not muted")
} icon: {
Image(systemName: "bell.fill")
}
.foregroundStyle(Color.green)
}
}
}
#if !DISABLE_OMEMO
if (!contact.isMuc || (contact.isMuc && contact.mucType == kMucTypeGroup)) && !HelperTools.isContactBlacklistedForEncryption(contact.obj) {
Button {
if contact.isEncrypted {
showingShouldDisableEncryptionAlert = true
} else {
showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted)
}
} label: {
if contact.isEncrypted {
Label {
Text("Messages are encrypted")
} icon: {
Image(systemName: "lock.fill")
}
.foregroundStyle(Color.green)
} else {
Label {
Text("Messages are NOT encrypted")
} icon: {
Image(systemName: "lock.open.fill")
}
.foregroundStyle(Color.red)
}
}
.alert(isPresented: $showingCannotEncryptAlert) {
Alert(title: Text("Encryption Not Supported"), message: Text("This contact does not appear to have any devices that support encryption, please try again later if you think this is wrong."), dismissButton: .default(Text("Close")))
}
.actionSheet(isPresented: $showingShouldDisableEncryptionAlert) {
ActionSheet(
title: Text("Disable encryption?"),
message: Text("Do you really want to disable encryption for this contact?"),
buttons: [
.cancel(
Text("No, keep encryption activated"),
action: { }
),
.destructive(
Text("Yes, deactivate encryption"),
action: {
showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted)
}
)
]
)
}
//.buttonStyle(BorderlessButtonStyle())
}
#endif
if contact.isMuc && ownAffiliation == kMucAffiliationOwner {
let label = contact.obj.mucType == kMucTypeGroup ? NSLocalizedString("Rename Group", comment:"") : NSLocalizedString("Rename Channel", comment:"")
TextField(label, text: $contact.fullNameView, onEditingChanged: {
isEditingNickname = $0
})
.accessibilityLabel(contact.obj.mucType == kMucTypeGroup ? Text("Group name") : Text("Channel name"))
.addClearButton(isEditing: isEditingNickname, text: $contact.fullNameView)
} else if !contact.isMuc && !contact.isSelfChat {
TextField(NSLocalizedString("Rename Contact", comment: "placeholder text in contact details"), text: $contact.nickNameView, onEditingChanged: {
isEditingNickname = $0
})
.accessibilityLabel(Text("Nickname"))
.addClearButton(isEditing: isEditingNickname, text: $contact.nickNameView)
}
Toggle(isOn: Binding(get: {
contact.isPinned
}, set: {
contact.obj.togglePinnedChat($0)
})) {
Text("Pin Chat")
}
#if !DISABLE_OMEMO
if !HelperTools.isContactBlacklistedForEncryption(contact.obj) && !contact.isSelfChat {
if !contact.isMuc || contact.mucType == kMucTypeGroup {
NavigationLink(destination: LazyClosureView(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: contact)))) {
Text("Encryption Keys")
}
}
}
#endif
if !contact.isMuc && !contact.isSelfChat {
NavigationLink(destination: LazyClosureView(ContactResources(contact: contact))) {
Text("Resources")
}
}
let accountJid = account.connectionProperties.identity.jid
let displayName = contact.contactDisplayName as String
let sharedUrl = HelperTools.getSharedDocumentsURL(forPathComponents:[accountJid, displayName])
if UIApplication.shared.canOpenURL(sharedUrl) && FileManager.default.fileExists(atPath:sharedUrl.path) {
NavigationLink(destination: LazyClosureView{MediaGalleryView(contact: contact.contactJid as String, accountID: contact.accountID)}) {
Text("Shared Media")
}
Button(action: {
UIApplication.shared.open(sharedUrl, options:[:])
}) {
Text("Shared Files")
}
}
NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact))) {
Text("Change Chat Background")
}
if contact.obj.isMuc && contact.obj.mucType == kMucTypeGroup {
NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) {
Text("Group Members")
}
} else if contact.obj.isMuc && contact.obj.mucType == kMucTypeChannel {
if [kMucAffiliationOwner, kMucAffiliationAdmin].contains(ownAffiliation) {
NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) {
Text("Channel Participants")
}
} else {
NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) {
Text("Channel Participants")
}
}
}
}
.listStyle(.plain)
Section { // the destructive section...
if !contact.isSelfChat {
Button(action: {
if !contact.isBlocked {
showingBlockContactConfirmation = true
} else {
showingCannotBlockAlert = !contact.obj.toggleBlocked(!contact.isBlocked)
}
}) {
if !contact.isBlocked {
Text("Block Contact")
.foregroundColor(.red)
} else {
Text("Unblock Contact")
}
}
.alert(isPresented: $showingCannotBlockAlert) {
Alert(title: Text("Blocking/Unblocking Not Supported"), message: Text("The server does not support blocking (XEP-0191)."), dismissButton: .default(Text("Close")))
}
.actionSheet(isPresented: $showingBlockContactConfirmation) {
ActionSheet(
title: Text("Block Contact"),
message: Text("Do you really want to block this contact? You won't receive any messages from this contact."),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
showingCannotBlockAlert = !contact.obj.toggleBlocked(!contact.isBlocked)
}
)
]
)
}
Group {
if contact.isInRoster {
Button(action: {
showingRemoveContactConfirmation = true
}) {
if contact.isMuc {
if contact.mucType == kMucTypeGroup {
Text("Leave Group")
.foregroundColor(.red)
} else {
Text("Leave Channel")
.foregroundColor(.red)
}
} else {
Text("Remove from contacts")
.foregroundColor(.red)
}
}
.actionSheet(isPresented: $showingRemoveContactConfirmation) {
ActionSheet(
title: Text(contact.isMuc ? NSLocalizedString("Leave this conversation", comment: "") : String(format: NSLocalizedString("Remove %@ from contacts?", comment: ""), contact.contactJid)),
message: Text(contact.isMuc ? NSLocalizedString("You will no longer receive messages from this conversation", comment: "") : NSLocalizedString("They will no longer see when you are online. They may not be able to send you encrypted messages.", comment: "")),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
contact.obj.removeFromRoster() //this will dismiss the chatview via kMonalContactRemoved notification
//NOTE: since we can get opened from objc through active chats,
//NOTE: we still need to support our SheetDismisserProtocol
if let delegate = self.delegate {
delegate.dismiss()
} else {
self.presentationMode.wrappedValue.dismiss()
}
}
)
]
)
}
} else {
Button(action: {
showingAddContactConfirmation = true
}) {
if contact.isMuc {
if contact.mucType == kMucTypeGroup {
Text("Join Group")
} else {
Text("Join Channel")
}
} else {
Text("Add to contacts")
}
}
.actionSheet(isPresented: $showingAddContactConfirmation) {
ActionSheet(
title: Text(contact.isMuc ? (contact.mucType == kMucTypeGroup ? NSLocalizedString("Join Group", comment: "") : NSLocalizedString("Join Channel", comment: "")) : String(format: NSLocalizedString("Add %@ to your contacts?", comment: ""), contact.contactJid)),
message: Text(contact.isMuc ? NSLocalizedString("You will receive subsequent messages from this conversation", comment: "") : NSLocalizedString("They will see when you are online. They will be able to send you encrypted messages.", comment: "")),
buttons: [
.cancel(),
.default(
Text("Yes"),
action: {
contact.obj.addToRoster()
}
),
]
)
}
}
}
}
if ownAffiliation == kMucAffiliationOwner {
Section {
Button(action: {
showingDestroyConfirmation = true
}) {
if contact.mucType == kMucTypeGroup {
Text("Destroy Group").foregroundColor(.red)
} else {
Text("Destroy Channel").foregroundColor(.red)
}
}
.actionSheet(isPresented: $showingDestroyConfirmation) {
ActionSheet(
title: contact.mucType == kMucTypeGroup ? Text("Destroy Group") : Text("Destroy Channel"),
message: contact.mucType == kMucTypeGroup ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
showPromisingLoadingOverlay(overlay, headlineView:contact.mucType == kMucTypeGroup ? Text("Destroying group...") : Text("Destroying channel..."), descriptionView:Text("")) {
promisifyMucAction(account:account, mucJid:contact.contactJid) {
self.account.mucProcessor.destroyRoom(contact.contactJid as String)
}
}.done { callback in
if let callback = callback {
self.successCallback = callback
}
successAlert(title: Text("Success"), message: contact.mucType == kMucTypeGroup ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel."))
}.catch { error in
errorAlert(title: Text("Error destroying group!"), message: Text("\(String(describing:error))"))
}
}
)
]
)
}
}
}
Button(action: {
showingClearHistoryConfirmation = true
}) {
if contact.isMuc {
if contact.obj.mucType == kMucTypeGroup {
Text("Clear chat history of this group")
} else {
Text("Clear chat history of this channel")
}
} else {
Text("Clear chat history of this contact")
}
}
.foregroundColor(.red)
.actionSheet(isPresented: $showingClearHistoryConfirmation) {
ActionSheet(
title: Text("Clear History"),
message: Text("Do you really want to clear all messages exchanged in this conversation? If using OMEMO you won't even be able to load them from your server again."),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
contact.obj.clearHistory()
}
)
]
)
}
}
#if !DISABLE_OMEMO
//omemo debug stuff, should be removed in a few months
Section {
// only display omemo session reset button on 1:1 and private groups
if contact.obj.isMuc == false || (contact.isMuc && contact.mucType == kMucTypeGroup) {
Button(action: {
showingResetOmemoSessionConfirmation = true
}) {
Text("Reset OMEMO session")
.foregroundColor(.red)
}
.actionSheet(isPresented: $showingResetOmemoSessionConfirmation) {
ActionSheet(
title: Text("Reset OMEMO session"),
message: Text("Do you really want to reset the OMEMO session? You should only reset the connection if you know what you are doing!"),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
self.account.omemo.clearAllSessions(forJid:contact.contactJid);
}
)
]
)
}
}
}
#endif
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.tint(Color.primary)
.addLoadingOverlay(overlay)
.navigationBarTitle(contact.contactDisplayName as String, displayMode:.inline)
.alert(isPresented: $showAlert) {
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: {
showAlert = false
if self.success == true {
if let callback = self.successCallback {
callback()
}
//close muc ui and leave chat ui of this muc
if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats {
activeChats.presentChat(with:nil)
}
}
}))
}
.sheet(isPresented: $showingSheetEditSubject) {
LazyClosureView(EditGroupSubject(contact: contact))
}
.sheet(isPresented:$showingImagePicker) {
ImagePicker(image:$inputImage)
}
.sheet(isPresented: $inputImage.optionalMappedToBool()) {
ImageCropView(originalImage: inputImage!, configureBlock: { cropViewController in
cropViewController.aspectRatioPreset = .presetSquare
cropViewController.aspectRatioLockEnabled = true
cropViewController.aspectRatioPickerButtonHidden = true
cropViewController.resetAspectRatioEnabled = false
}, onCanceled: {
inputImage = nil
}) { (image, cropRect, angle) in
showPromisingLoadingOverlay(overlay, headlineView:Text("Uploading avatar..."), descriptionView:Text("")) {
promisifyMucAction(account:account, mucJid:contact.contactJid) {
self.account.mucProcessor.publishAvatar(image, forMuc: contact.contactJid)
}
}.catch { error in
errorAlert(title: Text("Error changing avatar!"), message: Text("\(String(describing:error))"))
hideLoadingOverlay(overlay)
}
}
}
.onChange(of:contact.avatar as UIImage) { _ in
hideLoadingOverlay(overlay)
}
.onAppear {
self.updateRoleAndAffiliation()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let notificationContact = notification.userInfo?["contact"] as? MLContact {
DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...")
if notificationContact == contact {
self.updateRoleAndAffiliation()
}
}
}
}
}
struct ContactDetails_Previews: PreviewProvider {
static var previews: some View {
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(0)))
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(1)))
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(2)))
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(3)))
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(4)))
}
}

View file

@ -0,0 +1,86 @@
//
// ContactEntry.swift
// Monal
//
// Created by Friedrich Altheide on 28.11.23.
// Copyright © 2023 monal-im.org. All rights reserved.
//
struct ContactEntry<AdditionalContent: View>: View {
let contact: ObservableKVOWrapper<MLContact>
let selfnotesPrefix: Bool
let fallback: String?
@ViewBuilder let additionalContent: () -> AdditionalContent
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool = true, fallback: String? = nil) where AdditionalContent == EmptyView {
self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:fallback, additionalContent:{ EmptyView() })
}
init(contact:ObservableKVOWrapper<MLContact>, fallback: String?) where AdditionalContent == EmptyView {
self.init(contact:contact, selfnotesPrefix:true, fallback:fallback, additionalContent:{ EmptyView() })
}
init(contact:ObservableKVOWrapper<MLContact>, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.init(contact:contact, selfnotesPrefix:true, additionalContent:additionalContent)
}
init(contact:ObservableKVOWrapper<MLContact>, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.init(contact:contact, selfnotesPrefix:true, fallback:fallback, additionalContent:additionalContent)
}
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:nil, additionalContent:additionalContent)
}
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.contact = contact
self.selfnotesPrefix = selfnotesPrefix
self.fallback = fallback
self.additionalContent = additionalContent
}
var body:some View {
ZStack(alignment: .topLeading) {
HStack(alignment: .center) {
Image(uiImage: contact.avatar)
.resizable()
.frame(width: 40, height: 40, alignment: .center)
VStack(alignment: .leading) {
if selfnotesPrefix {
// use the if to make sure this view gets updated if the contact display name changes
// (the condition is never false, because contactDisplayName can not be nil)
if (contact.contactDisplayName as String?) != nil {
Text(contact.obj.contactDisplayName(withFallback:fallback))
}
} else {
// use the if to make sure this view gets updated if the contact display name changes
// (the condition is never false, because contactDisplayNameWithoutSelfnotesPrefix can not be nil)
if (contact.contactDisplayNameWithoutSelfnotesPrefix as String?) != nil {
Text(contact.obj.contactDisplayName(withFallback:fallback, andSelfnotesPrefix:false))
}
}
additionalContent()
Text(contact.contactJid as String)
.foregroundColor(Color(UIColor.secondaryLabel))
.font(.footnote)
}
}
}
}
}
#Preview {
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(0)))
}
#Preview {
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(1)))
}
#Preview {
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(2)))
}
#Preview {
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(3)))
}

View file

@ -0,0 +1,135 @@
//
// ContactList.swift
// Monal
//
// Created by Jan on 15.12.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
import OrderedCollections
struct ContactPickerEntry: View {
let contact : ObservableKVOWrapper<MLContact>
let isPicked: Bool
let isExistingMember: Bool
var body:some View {
ZStack(alignment: .topLeading) {
HStack(alignment: .center) {
if(isExistingMember) {
Image(systemName: "checkmark.circle")
.foregroundColor(.gray)
} else if(isPicked) {
Image(systemName: "checkmark.circle")
.foregroundColor(.accentColor)
} else {
Image(systemName: "circle")
}
ContactEntry(contact: contact)
}
}
}
}
struct ContactPicker: View {
typealias completionType = (OrderedSet<ObservableKVOWrapper<MLContact>>)->Void
let account: xmpp
@Binding var returnedContacts: OrderedSet<ObservableKVOWrapper<MLContact>>
@State var selectedContacts: OrderedSet<ObservableKVOWrapper<MLContact>>
@State var searchText = ""
@State var isEditingSearchInput = false
let allowRemoval: Bool
let completion: completionType?
init(_ account: xmpp, initializeFrom contacts: OrderedSet<ObservableKVOWrapper<MLContact>>, allowRemoval: Bool = true, completion:completionType?) {
self.account = account
self.allowRemoval = allowRemoval
self.completion = completion
_selectedContacts = State(wrappedValue:OrderedSet())
//use a temporary storage because we don't have a binding to the outside world but use the completion handler
var storage = contacts
_returnedContacts = Binding(
get: { storage },
set: { storage = $0 }
)
buildPreselectedContacts(contacts)
DDLogError("self.allowRemoval = \(String(describing:self.allowRemoval))")
}
init(_ account: xmpp, binding returnedContacts: Binding<OrderedSet<ObservableKVOWrapper<MLContact>>>, allowRemoval: Bool = true) {
self.account = account
self.allowRemoval = allowRemoval
self.completion = nil
_selectedContacts = State(wrappedValue:OrderedSet())
_returnedContacts = returnedContacts
buildPreselectedContacts(returnedContacts.wrappedValue)
}
private mutating func buildPreselectedContacts(_ source: OrderedSet<ObservableKVOWrapper<MLContact>>) {
//build currently selected list of contacts
var contactsTmp: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
for contact in source {
contactsTmp.append(contact)
}
_selectedContacts = State(wrappedValue:contactsTmp)
}
private var allContacts: OrderedSet<ObservableKVOWrapper<MLContact>> {
//build list of all possible contacts on this account (excluding selfchat and other mucs)
var contactsTmp: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountID) {
contactsTmp.append(ObservableKVOWrapper(contact))
}
return contactsTmp
}
private var searchResults : OrderedSet<ObservableKVOWrapper<MLContact>> {
if searchText.isEmpty {
return self.allContacts
} else {
var filteredContacts: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
for contact in self.allContacts {
if (contact.contactDisplayName as String).lowercased().contains(searchText.lowercased()) ||
(contact.contactJid as String).contains(searchText.lowercased()) {
filteredContacts.append(contact)
}
}
return filteredContacts
}
}
var body: some View {
if(allContacts.isEmpty) {
Text("No contacts to show :(")
.navigationTitle("Contact Lists")
} else {
List(searchResults) { contact in
let contactIsSelected = self.selectedContacts.contains(contact);
let contactIsAlreadyMember = self.returnedContacts.contains(contact);
ContactPickerEntry(contact: contact, isPicked: contactIsSelected, isExistingMember: !(!contactIsAlreadyMember || allowRemoval))
.onTapGesture {
// only allow changes to members that are not already part of the group
if(!contactIsAlreadyMember || allowRemoval) {
if(contactIsSelected) {
self.selectedContacts.remove(contact)
} else {
self.selectedContacts.append(contact)
}
}
}
}
.searchable(text: $searchText, placement: .automatic, prompt: nil)
.listStyle(.inset)
.navigationBarTitle(Text("Contact Selection"), displayMode: .inline)
.onDisappear {
returnedContacts.removeAll()
for contact in selectedContacts {
returnedContacts.append(contact)
}
if let completion = completion {
completion(returnedContacts)
}
}
}
}
}

View file

@ -0,0 +1,120 @@
//
// ContactRequestsMenu.swift
// Monal
//
// Created by Jan on 27.10.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
struct ContactRequestsMenuEntry: View {
let contact : MLContact
@State private var isDeleted = false
var body: some View {
HStack {
Text(contact.contactJid)
.foregroundColor(.secondary)
Spacer()
Group {
Button {
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
appDelegate.openChat(of:contact)
} label: {
Image(systemName: "text.bubble")
.foregroundStyle(Color.primary)
}
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
.buttonStyle(BorderlessButtonStyle())
Button {
// deny request
MLXMPPManager.sharedInstance().remove(contact)
} label: {
Image(systemName: "trash.circle")
.foregroundStyle(Color.red)
}
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
.buttonStyle(BorderlessButtonStyle())
Button {
// accept request
MLXMPPManager.sharedInstance().add(contact)
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
appDelegate.openChat(of:contact)
} label: {
Image(systemName: "checkmark.circle")
.foregroundStyle(Color.green)
}
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
.buttonStyle(BorderlessButtonStyle())
}
.font(.largeTitle)
}
}
}
struct ContactRequestsMenu: View {
@State var pendingRequests: [xmpp:[MLContact]] = [:]
@State var enabledAccounts: [Int:xmpp] = [:]
func updateRequests() {
let requests = DataLayer.sharedInstance().allContactRequests() as! [MLContact]
enabledAccounts.removeAll()
for account in MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] {
enabledAccounts[account.accountID.intValue] = account
}
pendingRequests.removeAll()
for contact in requests {
//add only requests having an enabled (dubbed connected) account
//(should be a noop because allContactRequests() returns only enabled accounts)
if let account = enabledAccounts[contact.accountID.intValue] {
if pendingRequests[account] == nil {
pendingRequests[account] = []
}
pendingRequests[account]!.append(contact)
}
}
}
var body: some View {
Section(header: Text("Allowing someone to add you as a contact lets them see your profile picture and when you are online.")) {
if(pendingRequests.isEmpty) {
Text("No pending contact requests")
.foregroundColor(.secondary)
} else {
List {
ForEach(pendingRequests.sorted(by:{ $0.0.connectionProperties.identity.jid < $1.0.connectionProperties.identity.jid }), id: \.key) { account, requests in
if enabledAccounts.count == 1 {
ForEach(requests.indices, id: \.self) { idx in
ContactRequestsMenuEntry(contact: requests[idx])
}
} else {
Section(header: Text("Account: \(account.connectionProperties.identity.jid)")) {
ForEach(requests.indices, id: \.self) { idx in
ContactRequestsMenuEntry(contact: requests[idx])
}
}
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh")).receive(on: RunLoop.main)) { notification in
updateRequests()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved")).receive(on: RunLoop.main)) { notification in
updateRequests()
}
.onAppear {
updateRequests()
}
}
}
struct ContactRequestsMenu_Previews: PreviewProvider {
static var previews: some View {
ContactRequestsMenu()
}
}

View file

@ -0,0 +1,159 @@
//
// ContactResources.swift
// Monal
//
// Created by Friedrich Altheide on 24.12.21.
// Copyright © 2021 Monal.im. All rights reserved.
//
import OrderedCollections
@ViewBuilder
func resourceRowElement(_ k: String, _ v: some View, space: CGFloat = 5) -> some View {
HStack {
Text(k).font(.headline)
Spacer()
v.foregroundColor(.secondary)
}
}
struct ContactResources: View {
@StateObject var contact: ObservableKVOWrapper<MLContact>
@State var contactVersionInfos: [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>]
@State private var showCaps: String?
init(contact: ObservableKVOWrapper<MLContact>, previewMock: [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>]? = nil) {
_contact = StateObject(wrappedValue: contact)
if previewMock != nil {
self.contactVersionInfos = previewMock!
} else {
var tmpInfos:[String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] = [:]
for ressourceName in DataLayer.sharedInstance().resources(for: contact.obj) {
// load already known software version info from database
if let softwareInfo = DataLayer.sharedInstance().getSoftwareVersionInfo(forContact:contact.obj.contactJid, resource:ressourceName, andAccount:contact.obj.accountID) {
tmpInfos[ressourceName] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
}
}
self.contactVersionInfos = tmpInfos
}
}
var body: some View {
List {
ForEach(self.contactVersionInfos.sorted(by:{ $0.0 < $1.0 }), id: \.key) { key, versionInfo in
Section {
VStack {
resourceRowElement("Resource:", Text(versionInfo.resource as String))
resourceRowElement("Client Name:", Text(versionInfo.appName as String? ?? ""))
resourceRowElement("Client Version:", Text(versionInfo.appVersion as String? ?? ""))
resourceRowElement("OS:", Text(versionInfo.platformOs as String? ?? ""))
if let lastInteraction = versionInfo.lastInteraction as Date? {
if lastInteraction.timeIntervalSince1970 == 0 {
resourceRowElement("Last Interaction:", Text("Currently Online"))
} else {
resourceRowElement("Last Interaction:", Text(lastInteraction.formatted(date:.numeric, time:.standard)))
}
} else {
resourceRowElement("Last Interaction:", Text("unsupported"))
}
}
.onTapGesture(count: 2, perform: {
showCaps = versionInfo.resource
})
}
}
}
.richAlert(isPresented:$showCaps, title:Text("XMPP Capabilities")) { resource in
VStack(alignment: .leading) {
Text("The resource '\(resource)' has the following capabilities:")
.font(Font.body.weight(.semibold))
Spacer()
.frame(height: 20)
Section {
let capsVer = DataLayer.sharedInstance().getVerForUser(self.contact.contactJid, andResource:resource, onAccountID:self.contact.accountID)
Text("Caps hash: \(String(describing:capsVer))")
Divider()
if let capsSet = DataLayer.sharedInstance().getCapsforVer(capsVer, onAccountID:contact.obj.accountID) as? Set<String> {
let caps = Array(capsSet)
VStack(alignment: .leading) {
ForEach(caps, id: \.self) { cap in
Text(cap)
.font(.system(.footnote, design:.monospaced))
if cap != caps.last {
Divider()
}
}
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalXmppUserSoftWareVersionRefresh")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let softwareInfo = notification.userInfo?["versionInfo"] as? MLContactSoftwareVersionInfo {
DDLogVerbose("Got software version info from account \(xmppAccount)...")
if softwareInfo.fromJid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
DispatchQueue.main.async {
DDLogVerbose("Successfully matched software version info update to current contact: \(contact.obj)")
self.contactVersionInfos[softwareInfo.resource ?? ""] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalNewPresenceNotice")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let jid = notification.userInfo?["jid"] as? String, let resource = notification.userInfo?["resource"] as? String, let available = notification.userInfo?["available"] as? NSNumber {
DDLogVerbose("Got presence update from account \(xmppAccount)...")
if jid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
DispatchQueue.main.async {
DDLogVerbose("Successfully matched presence update to current contact: \(contact.obj)")
if available.boolValue {
if let softwareInfo = DataLayer.sharedInstance().getSoftwareVersionInfo(forContact:contact.obj.contactJid, resource:resource, andAccount:contact.obj.accountID) {
self.contactVersionInfos[resource] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
}
// query software version from contact
MLXMPPManager.sharedInstance().getEntitySoftWareVersion(for:contact.obj, andResource:resource)
} else {
self.contactVersionInfos[resource] = nil
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalLastInteractionUpdatedNotice")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let jid = notification.userInfo?["jid"] as? String, let resource = notification.userInfo?["resource"] as? String, notification.userInfo?["lastInteraction"] as? NSDate != nil {
DDLogVerbose("Got lastInteraction update from account \(xmppAccount)...")
if jid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
DispatchQueue.main.async {
DDLogVerbose("Successfully matched lastInteraction update to current contact: \(contact.obj)")
self.contactVersionInfos[resource]?.obj.lastInteraction = DataLayer.sharedInstance().lastInteraction(ofJid:self.contact.obj.contactJid, andResource:resource, forAccountID:contact.obj.accountID)
}
}
}
}
.onAppear {
DDLogVerbose("View will appear...")
let newTimeout = DispatchTime.now() + 1.0;
DispatchQueue.main.asyncAfter(deadline: newTimeout) {
DDLogVerbose("Refreshing software version info...")
for ressourceName in DataLayer.sharedInstance().resources(for: contact.obj) {
// query software version from contact
MLXMPPManager.sharedInstance().getEntitySoftWareVersion(for:contact.obj, andResource:ressourceName)
}
}
}
.navigationBarTitle(Text("Devices of \(contact.contactDisplayName as String)"), displayMode: .inline)
}
}
func previewMock() -> [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] {
var previewMock:[String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] = [:]
previewMock["m1"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m1", andAppName: "Monal", andAppVersion: "1.1.1", andPlatformOS: "ios", andLastInteraction: Date()))
previewMock["m2"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m2", andAppName: "Monal", andAppVersion: "1.1.2", andPlatformOS: "macOS", andLastInteraction: Date()))
previewMock["m3"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m3", andAppName: "Monal", andAppVersion: "1.1.2", andPlatformOS: "macOS", andLastInteraction: Date()))
return previewMock
}
struct ContactResources_Previews: PreviewProvider {
static var previews: some View {
ContactResources(contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(0)), previewMock:previewMock())
}
}

View file

@ -0,0 +1,194 @@
//
// ContactsView.swift
// Monal
//
// Created by Matthew Fennell <matthew@fennell.dev> on 10/08/2024.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import SwiftUI
struct ContactViewEntry: View {
private let contact: MLContact
@Binding private var selectedContactForContactDetails: ObservableKVOWrapper<MLContact>?
private let dismissWithContact: (MLContact) -> Void
@State private var shouldPresentRemoveContactAlert: Bool = false
private var removeContactButtonText: String {
if !isDeletable {
return "Cannot delete notes to self"
}
return contact.isMuc ? "Remove Conversation" : "Remove Contact"
}
private var removeContactConfirmationTitle: String {
contact.isMuc ? "Leave this converstion?" : "Remove \(contact.contactJid) from contacts?"
}
private var removeContactConfirmationDetail: String {
contact.isMuc ? "" : "They will no longer see when you are online. They may not be able to access your encryption keys."
}
private var isDeletable: Bool {
!contact.isSelfChat
}
init(contact: MLContact, selectedContactForContactDetails: Binding<ObservableKVOWrapper<MLContact>?>, dismissWithContact: @escaping (MLContact) -> Void) {
self.contact = contact
_selectedContactForContactDetails = selectedContactForContactDetails
self.dismissWithContact = dismissWithContact
}
var body: some View {
// Apple's list dividers only extend as far left as the left-most text in the view.
// This means, by default, that the dividers on this screen would not extend all the way to the left of the view.
// This combination of HStack with spacing of 0, and empty text at the left of the view, is a workaround to override this behaviour.
// See https://stackoverflow.com/a/76698909
HStack(spacing: 0) {
Text("").frame(maxWidth: 0)
Button(action: { dismissWithContact(contact) }) {
HStack {
ContactEntry(contact: ObservableKVOWrapper<MLContact>(contact))
Spacer()
Button {
selectedContactForContactDetails = ObservableKVOWrapper<MLContact>(contact)
} label: {
Image(systemName: "info.circle")
.imageScale(.large)
}
.accessibilityLabel("Open contact details")
}
}
}
.swipeActions(allowsFullSwipe: false) {
// We do not use a Button with destructive role here as we would like to display the confirmation dialog first.
// A destructive role would dismiss the row immediately, without waiting for the confirmation.
Button(removeContactButtonText) {
shouldPresentRemoveContactAlert = true
}
.tint(isDeletable ? .red : .gray)
.disabled(!isDeletable)
}
.confirmationDialog(removeContactConfirmationTitle, isPresented: $shouldPresentRemoveContactAlert, titleVisibility: .visible) {
Button(role: .cancel) {} label: {
Text("No")
}
Button(role: .destructive) {
MLXMPPManager.sharedInstance().remove(contact)
} label: {
Text("Yes")
}
} message: {
Text(removeContactConfirmationDetail)
}
}
}
struct ContactsView: View {
@ObservedObject private var contacts: Contacts
private let delegate: SheetDismisserProtocol
private let dismissWithContact: (MLContact) -> Void
@State private var searchText: String = ""
@State private var selectedContactForContactDetails: ObservableKVOWrapper<MLContact>? = nil
init(contacts: Contacts, delegate: SheetDismisserProtocol, dismissWithContact: @escaping (MLContact) -> Void) {
self.contacts = contacts
self.delegate = delegate
self.dismissWithContact = dismissWithContact
}
private static func shouldDisplayContact(contact: MLContact) -> Bool {
#if IS_QUICKSY
return true
#endif
return contact.isSubscribedTo || contact.hasOutgoingContactRequest || contact.isSubscribedFrom
}
private var contactList: [MLContact] {
contacts.contacts
.filter(ContactsView.shouldDisplayContact)
.sorted { ContactsView.sortingCriteria($0) < ContactsView.sortingCriteria($1) }
}
private var searchResults: [MLContact] {
if searchText.isEmpty { return contactList }
return contactList.filter { searchMatchesContact(contact: $0, search: searchText) }
}
private static func sortingCriteria(_ contact: MLContact) -> (String, String) {
(contact.contactDisplayName.lowercased(), contact.contactJid.lowercased())
}
private func searchMatchesContact(contact: MLContact, search: String) -> Bool {
let jid = contact.contactJid.lowercased()
let name = contact.contactDisplayName.lowercased()
let search = search.lowercased()
return jid.contains(search) || name.contains(search)
}
var body: some View {
List {
ForEach(searchResults, id: \.self) { contact in
ContactViewEntry(contact: contact, selectedContactForContactDetails: $selectedContactForContactDetails, dismissWithContact: dismissWithContact)
}
}
.animation(.default, value: contactList)
.navigationTitle("Contacts")
.listStyle(.plain)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.overlay {
if contactList.isEmpty {
ContentUnavailableShimView("You need friends for this ride", systemImage: "figure.wave", description: Text("Add new contacts with the + button above. Your friends will pop up here when they can talk"))
} else if searchResults.isEmpty {
ContentUnavailableShimView.search
}
}
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
NavigationLink(destination: CreateGroupMenu(delegate: SheetDismisserProtocol())) {
Image(systemName: "person.3.fill")
}
.accessibilityLabel("Create contact group")
NavigationLink(destination: AddContactMenu(delegate: SheetDismisserProtocol(), dismissWithNewContact: dismissWithContact)) {
Image(systemName: "person.fill.badge.plus")
.overlay { NumberlessBadge($contacts.requestCount) }
}
.accessibilityLabel(contacts.requestCount > 0 ? "Add contact (contact requests pending)" : "Add New Contact")
}
}
.sheet(item: $selectedContactForContactDetails) { selectedContact in
AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactDetails(delegate: delegate, contact: selectedContact)))
}
}
}
class Contacts: ObservableObject {
@Published var contacts: Set<MLContact>
@Published var requestCount: Int
private var subscriptions: Set<AnyCancellable> = Set()
init() {
contacts = Set(DataLayer.sharedInstance().contactList())
requestCount = DataLayer.sharedInstance().allContactRequests().count
subscriptions = [
NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved"))
.receive(on: DispatchQueue.main)
.sink { _ in self.refreshContacts() },
NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh"))
.receive(on: DispatchQueue.main)
.sink { _ in self.refreshContacts() }
]
}
private func refreshContacts() {
contacts = Set(DataLayer.sharedInstance().contactList())
requestCount = DataLayer.sharedInstance().allContactRequests().count
}
}

View file

@ -0,0 +1,49 @@
//
// ContentUnavailableShimView.swift
// Monal
//
// Created by Matthew Fennell <matthew@fennell.dev> on 05/08/2024.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import SwiftUI
struct ContentUnavailableShimView: View {
private var reason: String
private var systemImage: String
private var description: Text
init(_ reason: String, systemImage: String, description: Text) {
self.reason = reason
self.systemImage = systemImage
self.description = description
}
var body: some View {
if #available(iOS 17, *) {
ContentUnavailableView(reason, systemImage: systemImage, description: description)
} else {
VStack {
Image(systemName: systemImage)
.foregroundStyle(.secondary)
.font(.largeTitle)
.padding(.bottom, 4)
Text(reason)
.fontWeight(.bold)
.font(.title3)
description
.foregroundStyle(.secondary)
.font(.footnote)
.multilineTextAlignment(.center)
}
}
}
}
extension ContentUnavailableShimView {
static var search: ContentUnavailableShimView = ContentUnavailableShimView("No Results", systemImage: "magnifyingglass", description: Text("Check the spelling or try a new search."))
}
#Preview {
ContentUnavailableShimView("Cannot Display", systemImage: "iphone.homebutton.slash", description: Text("Cannot display for this reason."))
}

View file

@ -0,0 +1,141 @@
//
// AddContactMenu.swift
// Monal
//
// Created by Jan on 27.10.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
import OrderedCollections
struct CreateGroupMenu: View {
private var appDelegate: MonalAppDelegate
private var delegate: SheetDismisserProtocol
@State private var enabledAccounts: [xmpp]
@State private var selectedAccount: xmpp?
@State private var groupName: String = ""
@State private var showAlert = false
// note: dismissLabel is not accessed but defined at the .alert() section
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
@State private var selectedContacts: OrderedSet<ObservableKVOWrapper<MLContact>> = []
@State private var isEditingGroupName = false
@StateObject private var overlay = LoadingOverlayState()
init(delegate: SheetDismisserProtocol) {
self.appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
self.delegate = delegate
let enabledAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp]
self.enabledAccounts = enabledAccounts
_selectedAccount = State(wrappedValue: enabledAccounts.first)
}
private func errorAlert(title: Text, message: Text = Text("")) {
alertPrompt.title = title
alertPrompt.message = message
showAlert = true
}
// When a Form is placed inside a Popover, and the horizontal size class is regular, the spacing chosen by SwiftUI is incorrect.
// In particular, the spacing between the top of the first element and the navigation bar is too small, meaning the two overlap.
// This only happens when the view is inside a popover, and the horizontal size class is regular.
// Therefore, it is inconvenient to apply some manual spacing, as this we would have to work out in which situations it should be applied.
// Placing a Text view inside the header causes SwiftUI to add consistent spacing in all situations.
var popoverFormSpacingWorkaround: some View {
Text("")
}
var body: some View {
Form {
if enabledAccounts.isEmpty {
Text("Please make sure at least one account has connected before trying to create new group.")
.foregroundColor(.secondary)
}
else
{
Section(header: popoverFormSpacingWorkaround) {
if enabledAccounts.count > 1 {
Picker(selection: $selectedAccount, label: Text("Use account")) {
ForEach(Array(self.enabledAccounts.enumerated()), id: \.element) { idx, account in
Text(account.connectionProperties.identity.jid).tag(account as xmpp?)
}
}
.pickerStyle(.menu)
}
TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when creating new group"), text: $groupName, onEditingChanged: { isEditingGroupName = $0 })
.autocorrectionDisabled()
.autocapitalization(.none)
.addClearButton(isEditing: isEditingGroupName, text:$groupName)
Button(action: {
guard let generatedJid = self.selectedAccount!.mucProcessor.generateMucJid() else {
errorAlert(title: Text("Error creating group!"), message: Text("Your server does not provide a MUC component."))
return
}
showLoadingOverlay(overlay, headline: NSLocalizedString("Creating Group", comment: ""))
guard let roomJid = self.selectedAccount!.mucProcessor.createGroup(generatedJid) else {
//room already existing in our local bookmarks --> just open it
//this should never happen since we randomly generated a jid above
hideLoadingOverlay(overlay)
let groupContact = MLContact.createContact(fromJid: generatedJid, andAccountID: self.selectedAccount!.accountID)
self.delegate.dismissWithoutAnimation()
if let activeChats = self.appDelegate.activeChats {
activeChats.presentChat(with:groupContact)
}
return
}
self.selectedAccount!.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary
let success : Bool = data["success"] as! Bool;
if success {
DataLayer.sharedInstance().setFullName(self.groupName, forContact:roomJid, andAccount:self.selectedAccount!.accountID)
self.selectedAccount!.mucProcessor.changeName(ofMuc: roomJid, to: self.groupName)
for user in self.selectedContacts {
self.selectedAccount!.mucProcessor.setAffiliation(kMucAffiliationMember, ofUser: user.contactJid, inMuc: roomJid)
self.selectedAccount!.mucProcessor.inviteUser(user.contactJid, inMuc: roomJid)
}
let groupContact = MLContact.createContact(fromJid: roomJid, andAccountID: self.selectedAccount!.accountID)
hideLoadingOverlay(overlay)
self.delegate.dismissWithoutAnimation()
if let activeChats = self.appDelegate.activeChats {
activeChats.presentChat(with:groupContact)
}
} else {
hideLoadingOverlay(overlay)
errorAlert(title: Text("Error creating group!"), message: Text(data["errorMessage"] as! String))
}
}, forMuc: roomJid)
}, label: {
Text("Create new group")
})
}
Section(header: Text("Selected Group Members")) {
NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) {
Text("Change Group Members")
}
ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in
ContactEntry(contact: contact)
}
.onDelete(perform: { indexSet in
self.selectedContacts.remove(at: indexSet.first!)
})
}
}
}
.alert(isPresented: $showAlert) {
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: {
showAlert = false
}))
}
.addLoadingOverlay(overlay)
.navigationBarTitle(Text("Create new group"), displayMode: .inline)
}
}
struct CreateGroupMenu_Previews: PreviewProvider {
static var delegate = SheetDismisserProtocol()
static var previews: some View {
CreateGroupMenu(delegate: delegate)
}
}

Some files were not shown because too many files have changed in this diff Show more